windowcapture
исходный код / Helpers/CursorReader.cs

CursorReader.cs

232 строк · 11,321 байт · модуль Helpers
  1using System;
  2using System.Runtime.InteropServices;
  3using System.Text;
  4using WindowCapture.Native;
  5
  6namespace WindowCapture.Helpers
  7{
  8    /// <summary>
  9    /// CursorReader: reads text around the caret in the active application.
 10    /// Uses UI Automation TextPattern (Chrome, WPF, UWP) with WM_GETTEXT fallback (Notepad, Win32).
 11    /// Non-invasive: doesn't touch clipboard, doesn't move cursor.
 12    /// </summary>
 13    public static class CursorReader
 14    {
 15        // Cached context (updated on STA thread)
 16        private static string cachedLine = "";       // current line text
 17        private static string cachedWord = "";       // word under cursor
 18        private static string[] cachedPrevWords;     // previous N words
 19        private static int cachedCaretInLine = -1;   // caret position within line
 20        private static long lastUpdateTick = 0;
 21        private static volatile bool updating = false;
 22
 23        const int UPDATE_INTERVAL_MS = 200; // refresh every 200ms max
 24
 25        // P/Invoke for WM_GETTEXT approach
 26        const int WM_GETTEXTLENGTH = 0x000E;
 27        const int WM_GETTEXT_MSG = 0x000D;
 28        const int EM_GETSEL = 0x00B0;
 29        const int EM_LINEFROMCHAR = 0x00C9;
 30        const int EM_LINEINDEX = 0x00BB;
 31        const int EM_LINELENGTH = 0x00C1;
 32        const int EM_GETLINE = 0x00C4;
 33
 34        [DllImport("user32.dll", CharSet = CharSet.Unicode)]
 35        static extern IntPtr SendMessageW(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
 36        [DllImport("user32.dll", CharSet = CharSet.Unicode)]
 37        static extern IntPtr SendMessageW(IntPtr hWnd, int msg, IntPtr wParam, StringBuilder lParam);
 38
 39        /// <summary>Result of cursor context analysis.</summary>
 40        public class CursorContext
 41        {
 42            public string CurrentWord;     // word at/before caret (possibly partial)
 43            public string CurrentLine;     // full text of current line
 44            public string[] PrevWords;     // previous words (up to 5)
 45            public int CaretInLine;        // caret position within line
 46            public bool IsValid;           // whether we got meaningful data
 47        }
 48
 49        /// <summary>
 50        /// Get cursor context. Called from any thread.
 51        /// Returns cached result and schedules background refresh.
 52        /// </summary>
 53        public static CursorContext GetContext()
 54        {
 55            long now = Environment.TickCount;
 56            if (!updating && (now - lastUpdateTick > UPDATE_INTERVAL_MS))
 57            {
 58                updating = true;
 59                var t = new System.Threading.Thread(DoUpdate);
 60                t.SetApartmentState(System.Threading.ApartmentState.STA);
 61                t.IsBackground = true;
 62                t.Start();
 63            }
 64
 65            return new CursorContext
 66            {
 67                CurrentWord = cachedWord,
 68                CurrentLine = cachedLine,
 69                PrevWords = cachedPrevWords,
 70                CaretInLine = cachedCaretInLine,
 71                IsValid = !string.IsNullOrEmpty(cachedLine)
 72            };
 73        }
 74
 75        /// <summary>Force synchronous update (for Ctrl+Shift+Space).</summary>
 76        public static CursorContext GetContextSync()
 77        {
 78            var t = new System.Threading.Thread(DoUpdate);
 79            t.SetApartmentState(System.Threading.ApartmentState.STA);
 80            t.IsBackground = true;
 81            t.Start();
 82            t.Join(500); // wait up to 500ms
 83            return GetContext();
 84        }
 85
 86        private static void DoUpdate()
 87        {
 88            try
 89            {
 90                lastUpdateTick = Environment.TickCount;
 91                string line = null;
 92                int caretPos = -1;
 93
 94                // Method 1: UI Automation TextPattern
 95                try
 96                {
 97                    var focused = System.Windows.Automation.AutomationElement.FocusedElement;
 98                    if (focused != null)
 99                    {
100                        object patObj;
101                        if (focused.TryGetCurrentPattern(System.Windows.Automation.TextPattern.Pattern, out patObj))
102                        {
103                            var tp = patObj as System.Windows.Automation.TextPattern;
104                            if (tp != null)
105                            {
106                                var sels = tp.GetSelection();
107                                if (sels != null && sels.Length > 0)
108                                {
109                                    // Get current line
110                                    var lineRange = sels[0].Clone();
111                                    lineRange.ExpandToEnclosingUnit(System.Windows.Automation.Text.TextUnit.Line);
112                                    line = lineRange.GetText(1000);
113
114                                    // Get position within line: count chars from line start to caret
115                                    var beforeCaret = sels[0].Clone();
116                                    beforeCaret.MoveEndpointByUnit(
117                                        System.Windows.Automation.Text.TextPatternRangeEndpoint.Start,
118                                        System.Windows.Automation.Text.TextUnit.Line, -1);
119                                    string beforeText = beforeCaret.GetText(1000);
120                                    // caretPos = chars from start of line to caret
121                                    if (line != null && beforeText != null)
122                                    {
123                                        int lineStart = beforeText.LastIndexOf('\n');
124                                        caretPos = lineStart >= 0 ? beforeText.Length - lineStart - 1 : beforeText.Length;
125                                    }
126                                }
127                            }
128                        }
129                    }
130                }
131                catch { }
132
133                // Method 2: WM_GETTEXT + EM_GETSEL fallback (Notepad, Win32 edit)
134                if (string.IsNullOrEmpty(line))
135                {
136                    try
137                    {
138                        IntPtr fg = WinApi.GetForegroundWindow();
139                        if (fg != IntPtr.Zero)
140                        {
141                            uint pid; uint tid = WinApi.GetWindowThreadProcessId(fg, out pid);
142                            var info = new WinApi.GUITHREADINFO();
143                            info.cbSize = Marshal.SizeOf(typeof(WinApi.GUITHREADINFO));
144                            if (WinApi.GetGUIThreadInfo(tid, ref info))
145                            {
146                                IntPtr hwnd = info.hwndFocus != IntPtr.Zero ? info.hwndFocus : fg;
147
148                                // Get caret position
149                                IntPtr selResult = SendMessageW(hwnd, EM_GETSEL, IntPtr.Zero, IntPtr.Zero);
150                                int selStart = (int)(selResult.ToInt64() & 0xFFFF);
151
152                                // Get line number from caret
153                                int lineNum = (int)SendMessageW(hwnd, EM_LINEFROMCHAR, (IntPtr)selStart, IntPtr.Zero);
154
155                                // Get line start index
156                                int lineIdx = (int)SendMessageW(hwnd, EM_LINEINDEX, (IntPtr)lineNum, IntPtr.Zero);
157
158                                // Get line length
159                                int lineLen = (int)SendMessageW(hwnd, EM_LINELENGTH, (IntPtr)lineIdx, IntPtr.Zero);
160
161                                if (lineLen > 0 && lineLen < 5000)
162                                {
163                                    // Get line text
164                                    var sb = new StringBuilder(lineLen + 4);
165                                    sb.Append((char)lineLen); // EM_GETLINE requires first word = buffer size
166                                    sb.Append('\0', lineLen + 2);
167                                    SendMessageW(hwnd, EM_GETLINE, (IntPtr)lineNum, sb);
168                                    line = sb.ToString().Substring(0, Math.Min(sb.Length, lineLen));
169                                    caretPos = selStart - lineIdx;
170                                }
171                                else
172                                {
173                                    // Fallback: WM_GETTEXT full text
174                                    int textLen = (int)SendMessageW(hwnd, WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero);
175                                    if (textLen > 0 && textLen < 50000)
176                                    {
177                                        var sb2 = new StringBuilder(textLen + 1);
178                                        SendMessageW(hwnd, WM_GETTEXT_MSG, (IntPtr)(textLen + 1), sb2);
179                                        string fullText = sb2.ToString();
180                                        // Find current line
181                                        if (selStart >= 0 && selStart <= fullText.Length)
182                                        {
183                                            int lineStart = fullText.LastIndexOf('\n', Math.Max(0, selStart - 1));
184                                            if (lineStart < 0) lineStart = 0; else lineStart++;
185                                            int lineEnd = fullText.IndexOf('\n', selStart);
186                                            if (lineEnd < 0) lineEnd = fullText.Length;
187                                            line = fullText.Substring(lineStart, lineEnd - lineStart);
188                                            caretPos = selStart - lineStart;
189                                        }
190                                    }
191                                }
192                            }
193                        }
194                    }
195                    catch { }
196                }
197
198                // Parse results
199                if (!string.IsNullOrEmpty(line))
200                {
201                    cachedLine = line.TrimEnd('\r', '\n');
202                    cachedCaretInLine = Math.Max(0, Math.Min(caretPos, cachedLine.Length));
203
204                    // Extract current word at caret
205                    string textBeforeCaret = cachedCaretInLine <= cachedLine.Length
206                        ? cachedLine.Substring(0, cachedCaretInLine)
207                        : cachedLine;
208
209                    // Find word boundaries
210                    var words = new System.Collections.Generic.List<string>();
211                    var wb = new StringBuilder();
212                    foreach (char c in textBeforeCaret)
213                    {
214                        if (char.IsLetter(c) || c == '-' || c == '\'')
215                            wb.Append(c);
216                        else if (wb.Length > 0)
217                        { words.Add(wb.ToString()); wb.Clear(); }
218                    }
219                    if (wb.Length > 0) words.Add(wb.ToString());
220
221                    cachedWord = words.Count > 0 ? words[words.Count - 1].ToLower() : "";
222                    int prevCount = Math.Min(words.Count - 1, 5);
223                    cachedPrevWords = new string[prevCount];
224                    for (int i = 0; i < prevCount; i++)
225                        cachedPrevWords[i] = words[words.Count - 1 - prevCount + i].ToLower();
226                }
227            }
228            catch { }
229            finally { updating = false; }
230        }
231    }
232}