【问题标题】:Detecting text changes in Word 2016 from VSTO add-in从 VSTO 加载项检测 Word 2016 中的文本更改
【发布时间】:2015-09-24 20:48:22
【问题描述】:

此问题与How to get the “KeyPress” event from a Word 2010 Addin (developed in C#)? 密切相关(实际上包括该问题答案中的示例代码),但这是专门针对在 Windows 10 中运行的 Word 2016 在 Visual Studio (Professional) 2015 中开发的。

我正在尝试检测 Word 文档中的文本何时从 VSTO 加载项发生更改。我明白了

没有事件驱动的方法可以做到这一点。 Word 根本不会在文本更改时发送事件。

我看到了两种解决方法:

  1. 使用WindowSelectionChange 事件。不幸的是,当通过按箭头键、使用鼠标、执行撤消或重做以及可能的其他操作更改选择时,似乎会发送此事件,但在键入或删除时不会发送。
  2. 使用低级 keydown 事件挂钩。这已经在 StackOverflow 的几个问题中进行了讨论,并且在 2014 年 2 月的 a thread on a Visual Studio forum 中也被称为“广泛传播的技术”。

我正在尝试使用How to get the “KeyPress” event from a Word 2010 Addin (developed in C#)? 的答案中的代码,它似乎可以观察到每个按键事件除了那些发送到 Word 2016 的事件。

这是我使用的代码,方便参考。

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;

namespace KeydownWordAddIn
{
    public partial class ThisAddIn
    {
        private const int WH_KEYBOARD_LL = 13;
        private const int WM_KEYDOWN = 0x0100;

        private static IntPtr hookId = IntPtr.Zero;
        private delegate IntPtr HookProcedure(int nCode, IntPtr wParam, IntPtr lParam);
        private static HookProcedure procedure = HookCallback;

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr GetModuleHandle(string lpModuleName);

        [DllImport("user32.dll", SetLastError = true)]
        private static extern bool UnhookWindowsHookEx(IntPtr hhk);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr SetWindowsHookEx(int idHook, HookProcedure lpfn, IntPtr hMod, uint dwThreadId);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

        private static IntPtr SetHook(HookProcedure procedure)
        {
            using (Process process = Process.GetCurrentProcess())
            using (ProcessModule module = process.MainModule)
                return SetWindowsHookEx(WH_KEYBOARD_LL, procedure, GetModuleHandle(module.ModuleName), 0);
        }

        private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
            {
                int pointerCode = Marshal.ReadInt32(lParam);
                string pressedKey = ((Keys)pointerCode).ToString();

                // Do some sort of processing on key press.
                var thread = new Thread(() => {
                    Debug.WriteLine(pressedKey);
                });
                thread.Start();
            }
            return CallNextHookEx(hookId, nCode, wParam, lParam);
        }

        private void ThisAddIn_Startup(object sender, EventArgs e)
        {
            hookId = SetHook(procedure);
        }

        private void ThisAddIn_Shutdown(object sender, EventArgs e)
        {
            UnhookWindowsHookEx(hookId);
        }

        #region VSTO generated code
        /// <summary>
        /// Required method for Designer support.
        /// </summary>
        private void InternalStartup()
        {
            this.Startup += new System.EventHandler(ThisAddIn_Startup);
            this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
        }
        #endregion
    }
}

当我使用此加载项运行 Word 2016 时,我看到 keydown 事件发送到 Edge 浏览器甚至 Visual Studio,但没有发送到 Word 本身。

在 Word 2016 中是否以某种方式阻止了 keydown 挂钩,还是我做错了什么?

【问题讨论】:

    标签: ms-word vsto visual-studio-2015 office-addins


    【解决方案1】:

    如果您不在 VSTO 加载项中使用低级挂钩,一切都应该可以正常工作。

    [DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern int GetCurrentThreadId();
    
    const int WH_KEYBOARD = 2;
    
    private static IntPtr SetHook(HookProcedure procedure)
    {
        var threadId = (uint)SafeNativeMethods.GetCurrentThreadId();
        return SetWindowsHookEx(WH_KEYBOARD, procedure, IntPtr.Zero, threadId);
    }
    

    请注意,您可能还需要创建一个挂钩来拦截鼠标消息,因为可以仅通过鼠标交互(例如,通过功能区或上下文菜单复制和粘贴)来修改文档的文本。

    VSTO 示例

    这是一个完整的 VSTO 示例,包括键盘和鼠标钩子:

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using System.Runtime.InteropServices;
    using System.Text;
    using Office = Microsoft.Office.Core;
    
    namespace SampleAddinWithKeyboardHook
    {
        public partial class ThisAddIn
        {
            // NOTE: We need a backing field to prevent the delegate being garbage collected
            private SafeNativeMethods.HookProc _mouseProc;
            private SafeNativeMethods.HookProc _keyboardProc;
    
            private IntPtr _hookIdMouse;
            private IntPtr _hookIdKeyboard;
    
            private void ThisAddIn_Startup(object sender, EventArgs e)
            {
                _mouseProc = MouseHookCallback;
                _keyboardProc = KeyboardHookCallback;
    
                SetWindowsHooks();
            }
    
            private void ThisAddIn_Shutdown(object sender, EventArgs e)
            {
                UnhookWindowsHooks();
            }
    
            private void SetWindowsHooks()
            {
                uint threadId = (uint)SafeNativeMethods.GetCurrentThreadId();
    
                _hookIdMouse =
                    SafeNativeMethods.SetWindowsHookEx(
                        (int)SafeNativeMethods.HookType.WH_MOUSE,
                        _mouseProc,
                        IntPtr.Zero,
                        threadId);
    
                _hookIdKeyboard =
                    SafeNativeMethods.SetWindowsHookEx(
                        (int)SafeNativeMethods.HookType.WH_KEYBOARD,
                        _keyboardProc,
                        IntPtr.Zero,
                        threadId);
            }
    
            private void UnhookWindowsHooks()
            {
                SafeNativeMethods.UnhookWindowsHookEx(_hookIdKeyboard);
                SafeNativeMethods.UnhookWindowsHookEx(_hookIdMouse);
            }
    
            private IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
            {
                if (nCode >= 0)
                {
                    var mouseHookStruct =
                        (SafeNativeMethods.MouseHookStructEx)
                            Marshal.PtrToStructure(lParam, typeof(SafeNativeMethods.MouseHookStructEx));
    
                    // handle mouse message here
                    var message = (SafeNativeMethods.WindowMessages)wParam;
                    Debug.WriteLine(
                        "{0} event detected at position {1} - {2}",
                        message,
                        mouseHookStruct.pt.X,
                        mouseHookStruct.pt.Y);
                }
                return SafeNativeMethods.CallNextHookEx(
                    _hookIdKeyboard,
                    nCode,
                    wParam,
                    lParam);
            }
    
            private IntPtr KeyboardHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
            {
                if (nCode >= 0)
                {
                    // handle key message here
                    Debug.WriteLine("Key event detected.");
                }
    
                return SafeNativeMethods.CallNextHookEx(
                    _hookIdKeyboard,
                    nCode,
                    wParam,
                    lParam);
            }
    
            #region VSTO generated code
    
            /// <summary>
            /// Required method for Designer support.
            /// </summary>
            private void InternalStartup()
            {
                Startup += ThisAddIn_Startup;
                Shutdown += ThisAddIn_Shutdown;
            }
    
            #endregion
        }
    
        internal static class SafeNativeMethods
        {
            public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
    
            public enum HookType
            {
                WH_KEYBOARD = 2,
                WH_MOUSE = 7
            }
    
            public enum WindowMessages : uint
            {
                WM_KEYDOWN = 0x0100,
                WM_KEYFIRST = 0x0100,
                WM_KEYLAST = 0x0108,
                WM_KEYUP = 0x0101,
                WM_LBUTTONDBLCLK = 0x0203,
                WM_LBUTTONDOWN = 0x0201,
                WM_LBUTTONUP = 0x0202,
                WM_MBUTTONDBLCLK = 0x0209,
                WM_MBUTTONDOWN = 0x0207,
                WM_MBUTTONUP = 0x0208,
                WM_MOUSEACTIVATE = 0x0021,
                WM_MOUSEFIRST = 0x0200,
                WM_MOUSEHOVER = 0x02A1,
                WM_MOUSELAST = 0x020D,
                WM_MOUSELEAVE = 0x02A3,
                WM_MOUSEMOVE = 0x0200,
                WM_MOUSEWHEEL = 0x020A,
                WM_MOUSEHWHEEL = 0x020E,
                WM_RBUTTONDBLCLK = 0x0206,
                WM_RBUTTONDOWN = 0x0204,
                WM_RBUTTONUP = 0x0205,
                WM_SYSDEADCHAR = 0x0107,
                WM_SYSKEYDOWN = 0x0104,
                WM_SYSKEYUP = 0x0105
            }
    
            [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            public static extern IntPtr GetModuleHandle(string lpModuleName);
    
            [DllImport("user32.dll", SetLastError = true)]
            public static extern bool UnhookWindowsHookEx(IntPtr hhk);
    
            [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            public static extern IntPtr SetWindowsHookEx(
                int idHook,
                HookProc lpfn,
                IntPtr hMod,
                uint dwThreadId);
    
            [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            public static extern IntPtr CallNextHookEx(
                IntPtr hhk,
                int nCode,
                IntPtr wParam,
                IntPtr lParam);
    
            [DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
            public static extern int GetCurrentThreadId();
    
            [StructLayout(LayoutKind.Sequential)]
            public struct Point
            {
                public int X;
                public int Y;
    
                public Point(int x, int y)
                {
                    X = x;
                    Y = y;
                }
    
                public static implicit operator System.Drawing.Point(Point p)
                {
                    return new System.Drawing.Point(p.X, p.Y);
                }
    
                public static implicit operator Point(System.Drawing.Point p)
                {
                    return new Point(p.X, p.Y);
                }
            }
    
            [StructLayout(LayoutKind.Sequential)]
            public struct MouseHookStructEx
            {
                public Point pt;
                public IntPtr hwnd;
                public uint wHitTestCode;
                public IntPtr dwExtraInfo;
                public int MouseData;
            }
        }
    }
    

    VBE 插件示例

    这是 VBA 编辑器(VBE 插件)的工作示例:

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using System.Runtime.InteropServices;
    using System.Text;
    using Extensibility;
    
    namespace VbeAddin
    {
        [ComVisible(true)]
        [ProgId("VbeAddin.Connect")]
        [Guid("95840C70-5A1A-4EDB-B436-40E8BF030469")]
        public class Connect : StandardOleMarshalObject, IDTExtensibility2
        {
            // NOTE: We need a backing field to prevent the delegate being garbage collected
            private SafeNativeMethods.HookProc _mouseProc;
            private SafeNativeMethods.HookProc _keyboardProc;
    
            private IntPtr _hookIdMouse;
            private IntPtr _hookIdKeyboard;
    
            #region IDTExtensibility2 Members
    
            public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
            {
                _mouseProc = MouseHookCallback;
                _keyboardProc = KeyboardHookCallback;
    
                SetWindowsHooks();
            }
    
            public void OnDisconnection(ext_DisconnectMode removeMode, ref Array custom)
            {
                UnhookWindowsHooks();
            }
    
            public void OnAddInsUpdate(ref Array custom)
            {
            }
    
            public void OnStartupComplete(ref Array custom)
            {
            }
    
            public void OnBeginShutdown(ref Array custom)
            {
            }
    
            #endregion
    
            private void SetWindowsHooks()
            {
                uint threadId = (uint)SafeNativeMethods.GetCurrentThreadId();
    
                _hookIdMouse =
                    SafeNativeMethods.SetWindowsHookEx(
                        (int)SafeNativeMethods.HookType.WH_MOUSE,
                        _mouseProc,
                        IntPtr.Zero,
                        threadId);
    
                _hookIdKeyboard =
                    SafeNativeMethods.SetWindowsHookEx(
                        (int)SafeNativeMethods.HookType.WH_KEYBOARD,
                        _keyboardProc,
                        IntPtr.Zero,
                        threadId);
            }
    
            private void UnhookWindowsHooks()
            {
                SafeNativeMethods.UnhookWindowsHookEx(_hookIdKeyboard);
                SafeNativeMethods.UnhookWindowsHookEx(_hookIdMouse);
            }
    
            private IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
            {
                if (nCode >= 0)
                {
                    var mouseHookStruct =
                        (SafeNativeMethods.MouseHookStructEx)
                            Marshal.PtrToStructure(
                                lParam,
                                typeof(SafeNativeMethods.MouseHookStructEx));
    
                    // handle mouse message here
                    var message = (SafeNativeMethods.WindowMessages)wParam;
                    Debug.WriteLine(
                        "{0} event detected at position {1} - {2}",
                        message,
                        mouseHookStruct.pt.X,
                        mouseHookStruct.pt.Y);
                }
                return SafeNativeMethods.CallNextHookEx(
                    _hookIdKeyboard,
                    nCode,
                    wParam,
                    lParam);
            }
    
            private IntPtr KeyboardHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
            {
                if (nCode >= 0)
                {
                    // handle key message here
                    Debug.WriteLine("Key event detected.");
                }
                return SafeNativeMethods.CallNextHookEx(
                    _hookIdKeyboard,
                    nCode,
                    wParam,
                    lParam);
            }
        }
    
        internal static class SafeNativeMethods
        {
            public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
    
            public enum HookType
            {
                WH_KEYBOARD = 2,
                WH_MOUSE = 7
            }
    
            public enum WindowMessages : uint
            {
                WM_KEYDOWN = 0x0100,
                WM_KEYFIRST = 0x0100,
                WM_KEYLAST = 0x0108,
                WM_KEYUP = 0x0101,
                WM_LBUTTONDBLCLK = 0x0203,
                WM_LBUTTONDOWN = 0x0201,
                WM_LBUTTONUP = 0x0202,
                WM_MBUTTONDBLCLK = 0x0209,
                WM_MBUTTONDOWN = 0x0207,
                WM_MBUTTONUP = 0x0208,
                WM_MOUSEACTIVATE = 0x0021,
                WM_MOUSEFIRST = 0x0200,
                WM_MOUSEHOVER = 0x02A1,
                WM_MOUSELAST = 0x020D,
                WM_MOUSELEAVE = 0x02A3,
                WM_MOUSEMOVE = 0x0200,
                WM_MOUSEWHEEL = 0x020A,
                WM_MOUSEHWHEEL = 0x020E,
                WM_RBUTTONDBLCLK = 0x0206,
                WM_RBUTTONDOWN = 0x0204,
                WM_RBUTTONUP = 0x0205,
                WM_SYSDEADCHAR = 0x0107,
                WM_SYSKEYDOWN = 0x0104,
                WM_SYSKEYUP = 0x0105
            }
    
            [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            public static extern IntPtr GetModuleHandle(string lpModuleName);
    
            [DllImport("user32.dll", SetLastError = true)]
            public static extern bool UnhookWindowsHookEx(IntPtr hhk);
    
            [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            public static extern IntPtr SetWindowsHookEx(
                int idHook,
                HookProc lpfn,
                IntPtr hMod,
                uint dwThreadId);
    
            [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            public static extern IntPtr CallNextHookEx(
                IntPtr hhk,
                int nCode,
                IntPtr wParam,
                IntPtr lParam);
    
            [DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
            public static extern int GetCurrentThreadId();
    
            [StructLayout(LayoutKind.Sequential)]
            public struct Point
            {
                public int X;
                public int Y;
            }
    
            [StructLayout(LayoutKind.Sequential)]
            public struct MouseHookStructEx
            {
                public Point pt;
                public IntPtr hwnd;
                public uint wHitTestCode;
                public IntPtr dwExtraInfo;
                public int MouseData;
            }
        }
    }
    

    【讨论】:

    • 在那篇 MSDN 博客文章中,似乎使用 null hMod 和线程 ID 调用了 SetWindowsHookEx idHookWH_GETMESSAGEWH_MOUSE,但不是 @987654332 @(位于帖子底部附近)。当idHookWH_KEYBOARD_LL 时,帖子中对SetWindowsHookEx 的调用看起来与我使用的代码相同。无论如何,使用 null hMod 和线程 ID 调用 SetWindowsHookEx 似乎无法捕获 Word 2016 中的 keydown 事件。
    • 请注意,如果您想知道按下的键,我从wParam 得到它,或多或少地将Debug.WriteLine("Key event detected."); 替换为Debug.WriteLine(((System.Windows.Forms.Keys)wParam).ToString());。我不确定这是否安全,但它看起来可行。
    • @Sabuncu:是的,代码使用了钩子,一种拦截某些类型事件的机制(我们感兴趣的是这些事件)。使用低级挂钩,您可以在不同的较低级别上获取事件。
    • 使用非低级钩子,事件已经被解释并且你得到信息,例如关于关键事件的重复计数,直接(参见KeyboardProc callback function 的文档),而使用低级挂钩可以获得原始数据(参见LowLevelKeyboardProc callback function
    • @DirkVollmar 正在尝试使用示例答案代码。有人能解释一下为什么KeyboardHookCallback 方法会在单次按键后连续触发6 次吗?
    【解决方案2】:

    我在 Word 2013 中遇到了同样的问题,不得不想出一个有点“创造性”的解决方案。它使用diffplex 来监视活动文档文本的变化,并在它发生变化时触发事件。这不太理想,但我们会做我们必须做的事情来让事情顺利进行。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Xml.Linq;
    using Word = Microsoft.Office.Interop.Word;
    using Office = Microsoft.Office.Core;
    using Microsoft.Office.Tools.Word;
    using System.ComponentModel;
    
    namespace WordUtils {
        public class TextChangeDetector {
    
            public Word.Application Application;
            private BackgroundWorker bg;
    
            public delegate void TextChangeHandler(object sender, TextChangedEventArgs e);
            public event TextChangeHandler OnTextChanged;
    
            public TextChangeDetector(Word.Application app) {
                this.Application = app;
            }
    
            public void Start() {
                bg = new BackgroundWorker();
                bg.WorkerReportsProgress = true;
                bg.WorkerSupportsCancellation = true;
                bg.ProgressChanged += bg_ProgressChanged;
                bg.DoWork += bg_DoWork;
                bg.RunWorkerAsync(this.Application);
            }
    
            private void bg_ProgressChanged(object sender, ProgressChangedEventArgs e) {
                switch (e.ProgressPercentage) {
                    case 50: //change
                        if (OnTextChanged != null) {
                            OnTextChanged(this, new TextChangedEventArgs((char)e.UserState));
                        }
                        break;
                }
            }
    
            private void bg_DoWork(object sender, DoWorkEventArgs e) {
    
                Word.Application wordApp = e.Argument as Word.Application;
                BackgroundWorker bg = sender as BackgroundWorker;
                string lastPage = string.Empty;
    
                while (true) {
                    try {
                        if (Application.Documents.Count > 0) {
                            if (Application.ActiveDocument.Words.Count > 0) {
                                var currentPage = Application.ActiveDocument.Bookmarks["\\Page"].Range.Text;                         
    
                                if (currentPage != null && currentPage != lastPage) {
                                    var differ = new DiffPlex.Differ();
                                    var builder = new DiffPlex.DiffBuilder.InlineDiffBuilder(differ);                               
                                    var difference = builder.BuildDiffModel(lastPage, currentPage);
                                    var change = from d in difference.Lines where d.Type != DiffPlex.DiffBuilder.Model.ChangeType.Unchanged select d;
                                    if (change.Any()) {                                    
                                        bg.ReportProgress(50, change.Last().Text.Last());
                                    }
    
                                    lastPage = currentPage;
                                }
    
    
                            }
                        }
                    } catch (Exception) {
    
                    }
    
                    if (bg.CancellationPending) {
                        break;
                    }
                    System.Threading.Thread.Sleep(100);
                }
            }
    
            public void Stop() {
                if (bg != null && !bg.CancellationPending) {
                    bg.CancelAsync();
                }
            }
        }
    
        public class TextChangedEventArgs : EventArgs {
            public char Letter;
            public TextChangedEventArgs(char letter) {
                this.Letter = letter;
            }
        }
    }
    

    用法:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Xml.Linq;
    using Word = Microsoft.Office.Interop.Word;
    using Office = Microsoft.Office.Core;
    using Microsoft.Office.Tools.Word;
    using WordUtils;
    
    namespace WordAddIn1 {
        public partial class ThisAddIn {
            TextChangeDetector detector;
    
            private void ThisAddIn_Startup(object sender, System.EventArgs e) {
                detector = new TextChangeDetector(Application);
                detector.OnTextChanged += detector_OnTextChanged;
                detector.Start();
            }
    
            void detector_OnTextChanged(object sender, TextChangedEventArgs e) {
                Console.WriteLine(e.Letter);
            }
    
            private void ThisAddIn_Shutdown(object sender, System.EventArgs e) {
                detector.Stop();
            }
    
            #region VSTO generated code
    
            /// <summary>
            /// Required method for Designer support - do not modify
            /// the contents of this method with the code editor.
            /// </summary>
            private void InternalStartup() {
                this.Startup += new System.EventHandler(ThisAddIn_Startup);
                this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
            }
    
            #endregion
        }
    }
    

    【讨论】:

    • 它如何处理在页面中间插入的文本?如果我正确阅读它(我对 Word API 不是很熟悉),它只会在页面上的最后一个单词与上一次迭代的最后一个单词不匹配时触发,对吗?
    • 没错。这是一种天真的方法,但适用于我的场景。我将其更新为使用差异库 (diffplex) 来检测所有更改,从而处理插入。
    • 我还应该指出,它仅在文档更改时触发,因此如果您尝试检测 Shift、Ctrl、Alt 等按键,它将无法正常工作。
    • 仍然是有趣的高级方法。我希望看到一个低级的钩子工作..性能如何?我会移植这个想法来解决 VBE 对象模型中的 VBComponent 对象 - 与 OP 不同的问题,但我正在使用 OP 低级挂钩的修改版本来捕获活动 CodePane 和,简单来说就是每次击键触发解析器任务。
    • 性能其实还不错。处理器在为我打字时从未超过 1%。您可以修改后台线程中的睡眠时间以进一步限制检测率。希望对您有所帮助!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-16
    • 1970-01-01
    相关资源
    最近更新 更多