├── .gitignore
├── ImeSharp.Demo
├── Form1.Designer.cs
├── Form1.cs
├── ImeSharp.Demo.csproj
├── Program.cs
└── rd.xml
├── ImeSharp.sln
├── ImeSharp
├── IMEString.cs
├── ImeSharp.csproj
├── Imm32Manager.cs
├── ImmCompositionResultHandler.cs
├── InputMethod.cs
├── Native
│ ├── NativeMethods.cs
│ ├── NativeMethodsIMM32.cs
│ └── NativeValues.cs
├── SafeSystemMetrics.cs
├── TextInputCallbacks.cs
├── TextServicesContext.cs
├── TextServicesLoader.cs
└── TextStore.cs
├── LICENSE.txt
├── README.md
├── build.cake
└── nuget.config
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Tools pulled down during build
16 | .tools/
17 | .dotnet/
18 | .packages/
19 |
20 | # Build results
21 | [Dd]ebug/
22 | [Dd]ebugPublic/
23 | [Rr]elease/
24 | [Rr]eleases/
25 | x64/
26 | x86/
27 | bld/
28 | [Bb]in/
29 | [Oo]bj/
30 | [Ll]og/
31 |
32 | # Visual Studio 2015/2017 cache/options directory
33 | .vs/
34 | # Uncomment if you have tasks that create the project's static files in wwwroot
35 | #wwwroot/
36 |
37 | # Visual Studio 2017 auto generated files
38 | Generated\ Files/
39 |
40 | # MSTest test Results
41 | [Tt]est[Rr]esult*/
42 | [Bb]uild[Ll]og.*
43 |
44 | # NUNIT
45 | *.VisualState.xml
46 | TestResult.xml
47 |
48 | # Build Results of an ATL Project
49 | [Dd]ebugPS/
50 | [Rr]eleasePS/
51 | dlldata.c
52 |
53 | # Benchmark Results
54 | BenchmarkDotNet.Artifacts/
55 |
56 | # .NET Core
57 | project.lock.json
58 | project.fragment.lock.json
59 | artifacts/
60 | .dotnet/
61 |
62 | # StyleCop
63 | StyleCopReport.xml
64 |
65 | # Files built by Visual Studio
66 | *_i.c
67 | *_p.c
68 | *_i.h
69 | *.ilk
70 | *.meta
71 | *.obj
72 | *.iobj
73 | *.pch
74 | *.pdb
75 | *.ipdb
76 | *.pgc
77 | *.pgd
78 | *.rsp
79 | *.sbr
80 | *.tlb
81 | *.tli
82 | *.tlh
83 | *.tmp
84 | *.tmp_proj
85 | *.log
86 | *.vspscc
87 | *.vssscc
88 | .builds
89 | *.pidb
90 | *.svclog
91 | *.scc
92 |
93 | # Chutzpah Test files
94 | _Chutzpah*
95 |
96 | # Visual C++ cache files
97 | ipch/
98 | *.aps
99 | *.ncb
100 | *.opendb
101 | *.opensdf
102 | *.sdf
103 | *.cachefile
104 | *.VC.db
105 | *.VC.VC.opendb
106 |
107 | # Visual Studio profiler
108 | *.psess
109 | *.vsp
110 | *.vspx
111 | *.sap
112 |
113 | # Visual Studio Trace Files
114 | *.e2e
115 |
116 | # TFS 2012 Local Workspace
117 | $tf/
118 |
119 | # Guidance Automation Toolkit
120 | *.gpState
121 |
122 | # ReSharper is a .NET coding add-in
123 | _ReSharper*/
124 | *.[Rr]e[Ss]harper
125 | *.DotSettings.user
126 |
127 | # JustCode is a .NET coding add-in
128 | .JustCode
129 |
130 | # TeamCity is a build add-in
131 | _TeamCity*
132 |
133 | # DotCover is a Code Coverage Tool
134 | *.dotCover
135 |
136 | # AxoCover is a Code Coverage Tool
137 | .axoCover/*
138 | !.axoCover/settings.json
139 |
140 | # Visual Studio code coverage results
141 | *.coverage
142 | *.coveragexml
143 |
144 | # Visual Studio Code
145 | .vscode/*
146 | !.vscode/settings.json
147 | !.vscode/tasks.json
148 | !.vscode/launch.json
149 | !.vscode/extensions.json
150 |
151 | # NCrunch
152 | _NCrunch_*
153 | .*crunch*.local.xml
154 | nCrunchTemp_*
155 |
156 | # MightyMoose
157 | *.mm.*
158 | AutoTest.Net/
159 |
160 | # Web workbench (sass)
161 | .sass-cache/
162 |
163 | # Installshield output folder
164 | [Ee]xpress/
165 |
166 | # DocProject is a documentation generator add-in
167 | DocProject/buildhelp/
168 | DocProject/Help/*.HxT
169 | DocProject/Help/*.HxC
170 | DocProject/Help/*.hhc
171 | DocProject/Help/*.hhk
172 | DocProject/Help/*.hhp
173 | DocProject/Help/Html2
174 | DocProject/Help/html
175 |
176 | # Click-Once directory
177 | publish/
178 |
179 | # Publish Web Output
180 | *.[Pp]ublish.xml
181 | *.azurePubxml
182 | # Note: Comment the next line if you want to checkin your web deploy settings,
183 | # but database connection strings (with potential passwords) will be unencrypted
184 | *.pubxml
185 | *.publishproj
186 |
187 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
188 | # checkin your Azure Web App publish settings, but sensitive information contained
189 | # in these scripts will be unencrypted
190 | PublishScripts/
191 |
192 | # NuGet Packages
193 | *.nupkg
194 | # The packages folder can be ignored because of Package Restore
195 | **/[Pp]ackages/*
196 | # except build/, which is used as an MSBuild target.
197 | !**/[Pp]ackages/build/
198 | # Uncomment if necessary however generally it will be regenerated when needed
199 | #!**/[Pp]ackages/repositories.config
200 | # NuGet v3's project.json files produces more ignorable files
201 | *.nuget.props
202 | *.nuget.targets
203 |
204 | # Microsoft Azure Build Output
205 | csx/
206 | *.build.csdef
207 |
208 | # Microsoft Azure Emulator
209 | ecf/
210 | rcf/
211 |
212 | # Windows Store app package directories and files
213 | AppPackages/
214 | BundleArtifacts/
215 | Package.StoreAssociation.xml
216 | _pkginfo.txt
217 | *.appx
218 |
219 | # Visual Studio cache files
220 | # files ending in .cache can be ignored
221 | *.[Cc]ache
222 | # but keep track of directories ending in .cache
223 | !*.[Cc]ache/
224 |
225 | # Others
226 | ClientBin/
227 | ~$*
228 | *~
229 | *.dbmdl
230 | *.dbproj.schemaview
231 | *.jfm
232 | *.pfx
233 | *.publishsettings
234 | orleans.codegen.cs
235 |
236 | # Including strong name files can present a security risk
237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
238 | #*.snk
239 |
240 | # Since there are multiple workflows, uncomment next line to ignore bower_components
241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
242 | #bower_components/
243 |
244 | # RIA/Silverlight projects
245 | Generated_Code/
246 |
247 | # Backup & report files from converting an old project file
248 | # to a newer Visual Studio version. Backup files are not needed,
249 | # because we have git ;-)
250 | _UpgradeReport_Files/
251 | Backup*/
252 | UpgradeLog*.XML
253 | UpgradeLog*.htm
254 | ServiceFabricBackup/
255 | *.rptproj.bak
256 |
257 | # SQL Server files
258 | *.mdf
259 | *.ldf
260 | *.ndf
261 |
262 | # Business Intelligence projects
263 | *.rdl.data
264 | *.bim.layout
265 | *.bim_*.settings
266 | *.rptproj.rsuser
267 |
268 | # Microsoft Fakes
269 | FakesAssemblies/
270 |
271 | # GhostDoc plugin setting file
272 | *.GhostDoc.xml
273 |
274 | # Node.js Tools for Visual Studio
275 | .ntvs_analysis.dat
276 | node_modules/
277 |
278 | # Visual Studio 6 build log
279 | *.plg
280 |
281 | # Visual Studio 6 workspace options file
282 | *.opt
283 |
284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
285 | *.vbw
286 |
287 | # Visual Studio LightSwitch build output
288 | **/*.HTMLClient/GeneratedArtifacts
289 | **/*.DesktopClient/GeneratedArtifacts
290 | **/*.DesktopClient/ModelManifest.xml
291 | **/*.Server/GeneratedArtifacts
292 | **/*.Server/ModelManifest.xml
293 | _Pvt_Extensions
294 |
295 | # Paket dependency manager
296 | .paket/paket.exe
297 | paket-files/
298 |
299 | # FAKE - F# Make
300 | .fake/
301 |
302 | # JetBrains Rider
303 | .idea/
304 | *.sln.iml
305 |
306 | # CodeRush
307 | .cr/
308 |
309 | # Python Tools for Visual Studio (PTVS)
310 | __pycache__/
311 | *.pyc
312 |
313 | # Cake - Uncomment if you are using it
314 | # tools/**
315 | # !tools/packages.config
316 |
317 | # Tabs Studio
318 | *.tss
319 |
320 | # Telerik's JustMock configuration file
321 | *.jmconfig
322 |
323 | # BizTalk build output
324 | *.btp.cs
325 | *.btm.cs
326 | *.odx.cs
327 | *.xsd.cs
328 |
329 | # OpenCover UI analysis results
330 | OpenCover/
331 |
332 | # Azure Stream Analytics local run output
333 | ASALocalRun/
334 |
335 | # MSBuild Binary and Structured Log
336 | *.binlog
337 |
338 | # NVidia Nsight GPU debugger configuration file
339 | *.nvuser
340 |
341 | # MFractors (Xamarin productivity tool) working folder
342 | .mfractor/
343 |
344 | ### OSX ###
345 |
346 | # General
347 | .DS_Store
348 | .AppleDouble
349 | .LSOverride
350 |
351 | # Icon must end with two \r
352 | Icon
353 |
354 |
355 | # Thumbnails
356 | ._*
357 |
358 | # Files that might appear in the root of a volume
359 | .DocumentRevisions-V100
360 | .fseventsd
361 | .Spotlight-V100
362 | .TemporaryItems
363 | .Trashes
364 | .VolumeIcon.icns
365 | .com.apple.timemachine.donotpresent
366 |
367 | # Directories potentially created on remote AFP share
368 | .AppleDB
369 | .AppleDesktop
370 | Network Trash Folder
371 | Temporary Items
372 | .apdisk
373 |
374 | # Project
375 | .vscode
376 |
377 | # Cake
378 | tools/**
379 | !tools/packages.config
380 |
--------------------------------------------------------------------------------
/ImeSharp.Demo/Form1.Designer.cs:
--------------------------------------------------------------------------------
1 | namespace ImeSharp.Demo
2 | {
3 | partial class Form1
4 | {
5 | ///
6 | /// 必需的设计器变量。
7 | ///
8 | private System.ComponentModel.IContainer components = null;
9 |
10 | ///
11 | /// 清理所有正在使用的资源。
12 | ///
13 | /// 如果应释放托管资源,为 true;否则为 false。
14 | protected override void Dispose(bool disposing)
15 | {
16 | if (disposing && (components != null))
17 | {
18 | components.Dispose();
19 | }
20 | base.Dispose(disposing);
21 | }
22 |
23 | #region Windows 窗体设计器生成的代码
24 |
25 | ///
26 | /// 设计器支持所需的方法 - 不要修改
27 | /// 使用代码编辑器修改此方法的内容。
28 | ///
29 | private void InitializeComponent()
30 | {
31 | this.components = new System.ComponentModel.Container();
32 |
33 | this.label1 = new System.Windows.Forms.Label();
34 | this.label2 = new System.Windows.Forms.Label();
35 | this.label4 = new System.Windows.Forms.Label();
36 | this.labelComp = new System.Windows.Forms.Label();
37 | this.label5 = new System.Windows.Forms.Label();
38 | this.textBoxCandidates = new System.Windows.Forms.TextBox();
39 | this.textBoxResult = new System.Windows.Forms.TextBox();
40 | this.SuspendLayout();
41 | //
42 | // label1
43 | //
44 | this.label1.AutoSize = true;
45 | this.label1.Location = new System.Drawing.Point(14, 51);
46 | this.label1.Name = "label1";
47 | this.label1.Size = new System.Drawing.Size(137, 12);
48 | this.label1.TabIndex = 0;
49 | this.label1.Text = "Press F1 to toggle IME";
50 | //
51 | // label2
52 | //
53 | this.label2.AutoSize = true;
54 | this.label2.Location = new System.Drawing.Point(14, 297);
55 | this.label2.Name = "label2";
56 | this.label2.Size = new System.Drawing.Size(77, 12);
57 | this.label2.TabIndex = 1;
58 | this.label2.Text = "InputResult:";
59 | //
60 | // label4
61 | //
62 | this.label4.AutoSize = true;
63 | this.label4.Location = new System.Drawing.Point(14, 100);
64 | this.label4.Name = "label4";
65 | this.label4.Size = new System.Drawing.Size(113, 12);
66 | this.label4.TabIndex = 3;
67 | this.label4.Text = "CompositionString:";
68 | //
69 | // labelComp
70 | //
71 | this.labelComp.AutoSize = true;
72 | this.labelComp.Location = new System.Drawing.Point(134, 99);
73 | this.labelComp.Name = "labelComp";
74 | this.labelComp.Size = new System.Drawing.Size(0, 12);
75 | this.labelComp.TabIndex = 4;
76 | //
77 | // label5
78 | //
79 | this.label5.AutoSize = true;
80 | this.label5.Location = new System.Drawing.Point(14, 124);
81 | this.label5.Name = "label5";
82 | this.label5.Size = new System.Drawing.Size(83, 12);
83 | this.label5.TabIndex = 5;
84 | this.label5.Text = "CandidateList";
85 | //
86 | // textBoxCandidates
87 | //
88 | this.textBoxCandidates.Enabled = false;
89 | this.textBoxCandidates.Location = new System.Drawing.Point(16, 140);
90 | this.textBoxCandidates.Multiline = true;
91 | this.textBoxCandidates.Name = "textBoxCandidates";
92 | this.textBoxCandidates.Size = new System.Drawing.Size(345, 139);
93 | this.textBoxCandidates.TabIndex = 6;
94 | //
95 | // textBoxResult
96 | //
97 | this.textBoxResult.Enabled = false;
98 | this.textBoxResult.Location = new System.Drawing.Point(97, 294);
99 | this.textBoxResult.Name = "textBoxResult";
100 | this.textBoxResult.Size = new System.Drawing.Size(264, 21);
101 | this.textBoxResult.TabIndex = 7;
102 | //
103 | // Form1
104 | //
105 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
106 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
107 | this.ClientSize = new System.Drawing.Size(800, 450);
108 | this.Controls.Add(this.textBoxResult);
109 | this.Controls.Add(this.textBoxCandidates);
110 | this.Controls.Add(this.label5);
111 | this.Controls.Add(this.labelComp);
112 | this.Controls.Add(this.label4);
113 | this.Controls.Add(this.label2);
114 | this.Controls.Add(this.label1);
115 | #if WINDOWS
116 | this.Name = "ImeSharp WinForms Demo";
117 | #else
118 | this.Name = "ImeSharp NetStandard Demo";
119 | #endif
120 | this.Text = this.Name;
121 | this.ResumeLayout(false);
122 | this.PerformLayout();
123 |
124 | }
125 |
126 | #endregion
127 |
128 | private System.Windows.Forms.Label label1;
129 | private System.Windows.Forms.Label label2;
130 | private System.Windows.Forms.Label label4;
131 | private System.Windows.Forms.Label labelComp;
132 | private System.Windows.Forms.Label label5;
133 | private System.Windows.Forms.TextBox textBoxCandidates;
134 | private System.Windows.Forms.TextBox textBoxResult;
135 | }
136 | }
137 |
138 |
--------------------------------------------------------------------------------
/ImeSharp.Demo/Form1.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.InteropServices;
3 | using System.Diagnostics;
4 | using System.Collections.Generic;
5 | using System.ComponentModel;
6 | using System.Data;
7 | using System.Drawing;
8 | using System.Linq;
9 | using System.Text;
10 | using System.Threading.Tasks;
11 | using System.Windows.Forms;
12 | using ImeSharp.Native;
13 | using NativeMessage = ImeSharp.Native.NativeMethods.NativeMessage;
14 |
15 | namespace ImeSharp.Demo
16 | {
17 | public partial class Form1 : Form
18 | {
19 | private string _inputContent = string.Empty;
20 | private DateTime _lastFakeDrawTime = DateTime.Now;
21 |
22 | private void OnTextInput(char character)
23 | {
24 | switch (character)
25 | {
26 | case '\b':
27 | if (_inputContent.Length > 0)
28 | _inputContent = _inputContent.Remove(_inputContent.Length - 1, 1);
29 | break;
30 | case '\r':
31 | _inputContent = "";
32 | break;
33 | default:
34 | _inputContent += character;
35 | break;
36 | }
37 |
38 | textBoxResult.Text = _inputContent;
39 | }
40 |
41 | private void OnTextComposition(string compositionText, int cursorPosition)
42 | {
43 | var str = compositionText.ToString();
44 | str = str.Insert(cursorPosition, "|");
45 | labelComp.Text = str;
46 |
47 | string candidateText = string.Empty;
48 |
49 | for (int i = 0; i < InputMethod.CandidatePageSize; i++)
50 | candidateText += string.Format(" {2}{0}.{1}\r\n", i + 1, InputMethod.CandidateList[i], i == InputMethod.CandidateSelection ? "*" : "");
51 |
52 | textBoxCandidates.Text = candidateText;
53 |
54 | InputMethod.SetTextInputRect(labelComp.Location.X + labelComp.Size.Width, labelComp.Location.Y, 0, labelComp.Size.Height);
55 | }
56 |
57 | public Form1()
58 | {
59 | InitializeComponent();
60 |
61 | CenterToScreen();
62 |
63 | Application.Idle += Application_Idle;
64 | KeyDown += Form1_KeyDown;
65 |
66 | InputMethod.Initialize(this.Handle, false);
67 | InputMethod.TextInputCallback = OnTextInput;
68 | InputMethod.TextCompositionCallback = OnTextComposition;
69 | }
70 |
71 | [DllImport("User32.dll", CharSet = CharSet.Auto)]
72 | private static extern bool PeekMessage(out NativeMessage msg, IntPtr hWnd, uint messageFilterMin, uint messageFilterMax, uint flags);
73 |
74 | // Mimic MonoGame game loop
75 | private void Application_Idle(object sender, EventArgs e)
76 | {
77 | var msg = new NativeMessage();
78 | do
79 | {
80 | FakeDraw();
81 | }
82 | while (!PeekMessage(out msg, IntPtr.Zero, 0, 0, 0) && IsDisposed == false);
83 |
84 | // Enables custom message pumping to fix frame stuck randomly.
85 | InputMethod.PumpMessage();
86 | }
87 |
88 | // Mimic MonoGame WndProc
89 | protected override void WndProc(ref Message m)
90 | {
91 | switch (m.Msg)
92 | {
93 | case NativeMethods.WM_KEYDOWN:
94 | //var now = DateTime.Now;
95 | //Debug.WriteLine("{0}:{1}.{2} - Form1.WndProc.WM_KEYDOWN {3}", now.ToShortTimeString(), now.Second, now.Millisecond, m.WParam);
96 | break;
97 | }
98 |
99 | base.WndProc(ref m);
100 | }
101 |
102 | private void Form1_KeyDown(object sender, KeyEventArgs e)
103 | {
104 | if (e.KeyCode == Keys.F1)
105 | InputMethod.Enabled = !InputMethod.Enabled;
106 | }
107 |
108 | private void FakeDraw()
109 | {
110 | var now = DateTime.Now;
111 | if (_lastFakeDrawTime.AddMilliseconds(50) < now)
112 | Console.WriteLine("{0}.{1}.{2} - SLOW FakeDraw!", now.ToShortTimeString(), now.Second, now.Millisecond);
113 |
114 | //label1.Text = Guid.NewGuid().ToString();
115 |
116 | _lastFakeDrawTime = now;
117 | }
118 |
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/ImeSharp.Demo/ImeSharp.Demo.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Exe
17 | net7.0-windows
18 | true
19 | true
20 | <_SuppressWinFormsTrimError>true
21 |
22 |
23 |
--------------------------------------------------------------------------------
/ImeSharp.Demo/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Runtime.InteropServices;
5 | using System.Threading.Tasks;
6 | using System.Windows.Forms;
7 |
8 | namespace ImeSharp.Demo
9 | {
10 | static class Program
11 | {
12 | ///
13 | /// The main entry point for the application.
14 | ///
15 | [STAThread]
16 | static void Main()
17 | {
18 | ComWrappers.RegisterForMarshalling(WinFormsComInterop.WinFormsComWrappers.Instance);
19 |
20 | //Application.SetHighDpiMode(HighDpiMode.SystemAware);
21 | Application.EnableVisualStyles();
22 | Application.SetCompatibleTextRenderingDefault(false);
23 | Application.Run(new Form1());
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/ImeSharp.Demo/rd.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/ImeSharp.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30413.136
5 | MinimumVisualStudioVersion = 15.0.26124.0
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImeSharp", "ImeSharp\ImeSharp.csproj", "{90542336-E164-4A9B-AE77-2820ABB4A342}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImeSharp.Demo", "ImeSharp.Demo\ImeSharp.Demo.csproj", "{664CE2A0-81D3-418B-B86D-642727AF7174}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Debug|x64 = Debug|x64
14 | Debug|x86 = Debug|x86
15 | Release|Any CPU = Release|Any CPU
16 | Release|x64 = Release|x64
17 | Release|x86 = Release|x86
18 | EndGlobalSection
19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
20 | {90542336-E164-4A9B-AE77-2820ABB4A342}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {90542336-E164-4A9B-AE77-2820ABB4A342}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {90542336-E164-4A9B-AE77-2820ABB4A342}.Debug|x64.ActiveCfg = Debug|Any CPU
23 | {90542336-E164-4A9B-AE77-2820ABB4A342}.Debug|x64.Build.0 = Debug|Any CPU
24 | {90542336-E164-4A9B-AE77-2820ABB4A342}.Debug|x86.ActiveCfg = Debug|Any CPU
25 | {90542336-E164-4A9B-AE77-2820ABB4A342}.Debug|x86.Build.0 = Debug|Any CPU
26 | {90542336-E164-4A9B-AE77-2820ABB4A342}.Release|Any CPU.ActiveCfg = Release|Any CPU
27 | {90542336-E164-4A9B-AE77-2820ABB4A342}.Release|Any CPU.Build.0 = Release|Any CPU
28 | {90542336-E164-4A9B-AE77-2820ABB4A342}.Release|x64.ActiveCfg = Release|Any CPU
29 | {90542336-E164-4A9B-AE77-2820ABB4A342}.Release|x64.Build.0 = Release|Any CPU
30 | {90542336-E164-4A9B-AE77-2820ABB4A342}.Release|x86.ActiveCfg = Release|Any CPU
31 | {90542336-E164-4A9B-AE77-2820ABB4A342}.Release|x86.Build.0 = Release|Any CPU
32 | {664CE2A0-81D3-418B-B86D-642727AF7174}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {664CE2A0-81D3-418B-B86D-642727AF7174}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {664CE2A0-81D3-418B-B86D-642727AF7174}.Debug|x64.ActiveCfg = Debug|Any CPU
35 | {664CE2A0-81D3-418B-B86D-642727AF7174}.Debug|x64.Build.0 = Debug|Any CPU
36 | {664CE2A0-81D3-418B-B86D-642727AF7174}.Debug|x86.ActiveCfg = Debug|Any CPU
37 | {664CE2A0-81D3-418B-B86D-642727AF7174}.Debug|x86.Build.0 = Debug|Any CPU
38 | {664CE2A0-81D3-418B-B86D-642727AF7174}.Release|Any CPU.ActiveCfg = Release|Any CPU
39 | {664CE2A0-81D3-418B-B86D-642727AF7174}.Release|Any CPU.Build.0 = Release|Any CPU
40 | {664CE2A0-81D3-418B-B86D-642727AF7174}.Release|x64.ActiveCfg = Release|Any CPU
41 | {664CE2A0-81D3-418B-B86D-642727AF7174}.Release|x64.Build.0 = Release|Any CPU
42 | {664CE2A0-81D3-418B-B86D-642727AF7174}.Release|x86.ActiveCfg = Release|Any CPU
43 | {664CE2A0-81D3-418B-B86D-642727AF7174}.Release|x86.Build.0 = Release|Any CPU
44 | EndGlobalSection
45 | GlobalSection(SolutionProperties) = preSolution
46 | HideSolutionNode = FALSE
47 | EndGlobalSection
48 | GlobalSection(ExtensibilityGlobals) = postSolution
49 | SolutionGuid = {1D8A758D-856B-422C-857E-863E77915742}
50 | EndGlobalSection
51 | EndGlobal
52 |
--------------------------------------------------------------------------------
/ImeSharp/IMEString.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 |
5 | namespace ImeSharp
6 | {
7 | public unsafe struct IMEString : IEnumerable
8 | {
9 | internal const int IMECharBufferSize = 64;
10 |
11 | public static readonly IMEString Empty = new IMEString((List)null);
12 |
13 | internal struct Enumerator : IEnumerator
14 | {
15 | private IMEString _imeString;
16 | private char _currentCharacter;
17 | private int _currentIndex;
18 |
19 | public Enumerator(IMEString imeString)
20 | {
21 | _imeString = imeString;
22 | _currentCharacter = '\0';
23 | _currentIndex = -1;
24 | }
25 |
26 | public bool MoveNext()
27 | {
28 | int size = _imeString.Count;
29 |
30 | _currentIndex++;
31 |
32 | if (_currentIndex == size)
33 | return false;
34 |
35 | fixed (char* ptr = _imeString.buffer)
36 | {
37 | _currentCharacter = *(ptr + _currentIndex);
38 | }
39 |
40 | return true;
41 | }
42 |
43 | public void Reset()
44 | {
45 | _currentIndex = -1;
46 | }
47 |
48 | public void Dispose()
49 | {
50 | }
51 |
52 | public char Current { get { return _currentCharacter; } }
53 | object IEnumerator.Current { get { return Current; } }
54 | }
55 |
56 | public int Count { get { return _size; } }
57 |
58 | public char this[int index]
59 | {
60 | get
61 | {
62 | if (index >= Count || index < 0)
63 | throw new ArgumentOutOfRangeException("index");
64 |
65 | fixed (char* ptr = buffer)
66 | {
67 | return *(ptr + index);
68 | }
69 | }
70 | }
71 |
72 | private int _size;
73 |
74 | fixed char buffer[IMECharBufferSize];
75 |
76 | public IMEString(string characters)
77 | {
78 | if (string.IsNullOrEmpty(characters))
79 | {
80 | _size = 0;
81 | return;
82 | }
83 |
84 | _size = characters.Length;
85 | if (_size > IMECharBufferSize)
86 | _size = IMECharBufferSize - 1;
87 |
88 | fixed (char* _ptr = buffer)
89 | {
90 | char* ptr = _ptr;
91 | for (var i = 0; i < _size; i++)
92 | {
93 | *ptr = characters[i];
94 | ptr++;
95 | }
96 | }
97 | }
98 |
99 | public IMEString(List characters)
100 | {
101 | if (characters == null || characters.Count == 0)
102 | {
103 | _size = 0;
104 | return;
105 | }
106 |
107 | _size = characters.Count;
108 | if (_size > IMECharBufferSize)
109 | _size = IMECharBufferSize - 1;
110 |
111 | fixed (char* _ptr = buffer)
112 | {
113 | char* ptr = _ptr;
114 | for (var i = 0; i < _size; i++)
115 | {
116 | *ptr = characters[i];
117 | ptr++;
118 | }
119 | }
120 | }
121 |
122 | public IMEString(char[] characters, int count)
123 | {
124 | if (characters == null || count <= 0)
125 | {
126 | _size = 0;
127 | return;
128 | }
129 |
130 | _size = count;
131 | if (_size > IMECharBufferSize)
132 | _size = IMECharBufferSize - 1;
133 |
134 | if (_size > characters.Length)
135 | _size = characters.Length;
136 |
137 | fixed (char* _ptr = buffer)
138 | {
139 | char* ptr = _ptr;
140 | for (var i = 0; i < _size; i++)
141 | {
142 | *ptr = characters[i];
143 | ptr++;
144 | }
145 | }
146 | }
147 |
148 | public IMEString(IntPtr bStrPtr)
149 | {
150 | if (bStrPtr == IntPtr.Zero)
151 | {
152 | _size = 0;
153 | return;
154 | }
155 |
156 | var ptrSrc = (char*)bStrPtr;
157 |
158 | int i = 0;
159 |
160 | fixed (char* _ptr = buffer)
161 | {
162 | char* ptr = _ptr;
163 |
164 | while (ptrSrc[i] != '\0')
165 | {
166 | *ptr = ptrSrc[i];
167 | i++;
168 | ptr++;
169 | }
170 | }
171 |
172 | _size = i;
173 | }
174 |
175 | public override string ToString()
176 | {
177 | fixed (char* ptr = buffer)
178 | {
179 | return new string(ptr, 0, _size);
180 | }
181 | }
182 |
183 | public IntPtr ToIntPtr()
184 | {
185 | fixed (char* ptr = buffer)
186 | {
187 | return (IntPtr)ptr;
188 | }
189 | }
190 |
191 | public IEnumerator GetEnumerator()
192 | {
193 | return new Enumerator(this);
194 | }
195 |
196 | IEnumerator IEnumerable.GetEnumerator()
197 | {
198 | return GetEnumerator();
199 | }
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/ImeSharp/ImeSharp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net46;netstandard2.0;netcoreapp3.1;net7.0;net8.0
5 | true
6 |
7 |
8 |
9 | C# wrapper for Windows IME APIs. Its goal is to support both IMM32 and TSF.
10 | IME;net8.0;winforms;windows;tsf;imm32
11 | ImeSharp
12 | https://github.com/ryancheung/ImeSharp
13 | https://github.com/ryancheung/ImeSharp
14 | ryancheung
15 | MIT
16 | README.md
17 |
18 |
19 |
20 | ..\Artifacts
21 | ImeSharp
22 | ImeSharp
23 | 5
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/ImeSharp/Imm32Manager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Buffers;
3 | using System.Runtime.InteropServices;
4 | using System.Globalization;
5 | using System.Diagnostics;
6 | using ImeSharp.Native;
7 |
8 | namespace ImeSharp
9 | {
10 | internal class Imm32Manager
11 | {
12 |
13 | // If the system is IMM enabled, this is true.
14 | private static bool _immEnabled = SafeSystemMetrics.IsImmEnabled;
15 |
16 | public static bool ImmEnabled { get { return _immEnabled; } }
17 |
18 | public const int LANG_CHINESE = 0x04;
19 | public const int LANG_KOREAN = 0x12;
20 | public const int LANG_JAPANESE = 0x11;
21 |
22 | public static int PRIMARYLANGID(int lgid)
23 | {
24 | return ((ushort)(lgid) & 0x3ff);
25 | }
26 |
27 | static Imm32Manager()
28 | {
29 | SetCurrentCulture();
30 | }
31 |
32 | ///
33 | /// return true if the current keyboard layout is a real IMM32-IME.
34 | ///
35 | public static bool IsImm32ImeCurrent()
36 | {
37 | if (!_immEnabled)
38 | return false;
39 |
40 | IntPtr hkl = NativeMethods.GetKeyboardLayout(0);
41 |
42 | return IsImm32Ime(hkl);
43 | }
44 |
45 | ///
46 | /// return true if the keyboard layout is a real IMM32-IME.
47 | ///
48 | public static bool IsImm32Ime(IntPtr hkl)
49 | {
50 | if (hkl == IntPtr.Zero)
51 | return false;
52 |
53 | return ((NativeMethods.IntPtrToInt32(hkl) & 0xf0000000) == 0xe0000000);
54 | }
55 |
56 | private static int _inputLanguageId;
57 |
58 | internal static void SetCurrentCulture()
59 | {
60 | var hkl = NativeMethods.GetKeyboardLayout(0);
61 | _inputLanguageId = NativeMethods.IntPtrToInt32(hkl) & 0xFFFF;
62 | }
63 |
64 | private IntPtr _windowHandle;
65 |
66 | private IntPtr _defaultImc;
67 | private IntPtr DefaultImc
68 | {
69 | get
70 | {
71 | if (_defaultImc == IntPtr.Zero)
72 | {
73 | IntPtr himc = NativeMethods.ImmCreateContext();
74 |
75 | // Store the default imc to _defaultImc.
76 | _defaultImc = himc;
77 | }
78 | return _defaultImc;
79 | }
80 | }
81 |
82 | private static ImmCompositionStringHandler _compositionStringHandler;
83 | private static ImmCompositionIntHandler _compositionCursorHandler;
84 |
85 | private bool _lastImmOpenStatus;
86 |
87 | public Imm32Manager(IntPtr windowHandle)
88 | {
89 | _windowHandle = windowHandle;
90 |
91 | _compositionStringHandler = new ImmCompositionStringHandler(DefaultImc, NativeMethods.GCS_COMPSTR);
92 | _compositionCursorHandler = new ImmCompositionIntHandler(DefaultImc, NativeMethods.GCS_CURSORPOS);
93 | }
94 |
95 | public static Imm32Manager Current
96 | {
97 | get
98 | {
99 | var defaultImm32Manager = InputMethod.DefaultImm32Manager;
100 |
101 | if (defaultImm32Manager == null)
102 | {
103 | defaultImm32Manager = new Imm32Manager(InputMethod.WindowHandle);
104 | InputMethod.DefaultImm32Manager = defaultImm32Manager;
105 | }
106 |
107 | return defaultImm32Manager;
108 | }
109 | }
110 |
111 | public void Enable()
112 | {
113 | if (DefaultImc != IntPtr.Zero)
114 | {
115 | // Create a temporary system caret
116 | NativeMethods.CreateCaret(_windowHandle, IntPtr.Zero, 2, 10);
117 | NativeMethods.ImmAssociateContext(_windowHandle, _defaultImc);
118 | }
119 | }
120 |
121 | public void Disable()
122 | {
123 | NativeMethods.ImmAssociateContext(_windowHandle, IntPtr.Zero);
124 | NativeMethods.DestroyCaret();
125 | }
126 |
127 | const int kCaretMargin = 1;
128 |
129 | // Set candidate window position.
130 | // Borrowed from https://github.com/chromium/chromium/blob/master/ui/base/ime/win/imm32_manager.cc
131 | public void SetCandidateWindow(TsfSharp.Rect caretRect)
132 | {
133 | int x = caretRect.Left;
134 | int y = caretRect.Top;
135 |
136 | if (PRIMARYLANGID(_inputLanguageId) == LANG_CHINESE)
137 | {
138 | // Chinese IMEs ignore function calls to ::ImmSetCandidateWindow()
139 | // when a user disables TSF (Text Service Framework) and CUAS (Cicero
140 | // Unaware Application Support).
141 | // On the other hand, when a user enables TSF and CUAS, Chinese IMEs
142 | // ignore the position of the current system caret and uses the
143 | // parameters given to ::ImmSetCandidateWindow() with its 'dwStyle'
144 | // parameter CFS_CANDIDATEPOS.
145 | // Therefore, we do not only call ::ImmSetCandidateWindow() but also
146 | // set the positions of the temporary system caret.
147 | var candidateForm = new NativeMethods.CANDIDATEFORM();
148 | candidateForm.dwStyle = NativeMethods.CFS_CANDIDATEPOS;
149 | candidateForm.ptCurrentPos.X = x;
150 | candidateForm.ptCurrentPos.Y = y;
151 | NativeMethods.ImmSetCandidateWindow(_defaultImc, ref candidateForm);
152 | }
153 |
154 | if (PRIMARYLANGID(_inputLanguageId) == LANG_JAPANESE)
155 | NativeMethods.SetCaretPos(x, caretRect.Bottom);
156 | else
157 | NativeMethods.SetCaretPos(x, y);
158 |
159 | // Set composition window position also to ensure move the candidate window position.
160 | var compositionForm = new NativeMethods.COMPOSITIONFORM();
161 | compositionForm.dwStyle = NativeMethods.CFS_POINT;
162 | compositionForm.ptCurrentPos.X = x;
163 | compositionForm.ptCurrentPos.Y = y;
164 | NativeMethods.ImmSetCompositionWindow(_defaultImc, ref compositionForm);
165 |
166 | if (PRIMARYLANGID(_inputLanguageId) == LANG_KOREAN)
167 | {
168 | // Chinese IMEs and Japanese IMEs require the upper-left corner of
169 | // the caret to move the position of their candidate windows.
170 | // On the other hand, Korean IMEs require the lower-left corner of the
171 | // caret to move their candidate windows.
172 | y += kCaretMargin;
173 | }
174 |
175 | // Need to return here since some Chinese IMEs would stuck if set
176 | // candidate window position with CFS_EXCLUDE style.
177 | if (PRIMARYLANGID(_inputLanguageId) == LANG_CHINESE) return;
178 |
179 | // Japanese IMEs and Korean IMEs also use the rectangle given to
180 | // ::ImmSetCandidateWindow() with its 'dwStyle' parameter CFS_EXCLUDE
181 | // to move their candidate windows when a user disables TSF and CUAS.
182 | // Therefore, we also set this parameter here.
183 | var excludeRectangle = new NativeMethods.CANDIDATEFORM();
184 | compositionForm.dwStyle = NativeMethods.CFS_EXCLUDE;
185 | compositionForm.ptCurrentPos.X = x;
186 | compositionForm.ptCurrentPos.Y = y;
187 | compositionForm.rcArea.Left = x;
188 | compositionForm.rcArea.Top = y;
189 | compositionForm.rcArea.Right = caretRect.Right;
190 | compositionForm.rcArea.Bottom = caretRect.Bottom;
191 | NativeMethods.ImmSetCandidateWindow(_defaultImc, ref excludeRectangle);
192 | }
193 |
194 | internal bool ProcessMessage(IntPtr hWnd, uint msg, ref IntPtr wParam, ref IntPtr lParam)
195 | {
196 | switch (msg)
197 | {
198 | case NativeMethods.WM_INPUTLANGCHANGE:
199 | SetCurrentCulture();
200 | break;
201 | case NativeMethods.WM_IME_SETCONTEXT:
202 | if (wParam.ToInt32() == 1 && InputMethod.Enabled)
203 | {
204 | // Must re-associate ime context or things won't work.
205 | NativeMethods.ImmAssociateContext(_windowHandle, DefaultImc);
206 |
207 | if (_lastImmOpenStatus)
208 | NativeMethods.ImmSetOpenStatus(DefaultImc, true);
209 |
210 | var lParam64 = lParam.ToInt64();
211 | if (!InputMethod.ShowOSImeWindow)
212 | lParam64 &= ~NativeMethods.ISC_SHOWUICANDIDATEWINDOW;
213 | else
214 | lParam64 &= ~NativeMethods.ISC_SHOWUICOMPOSITIONWINDOW;
215 | lParam = (IntPtr)(int)lParam64;
216 | }
217 | break;
218 | case NativeMethods.WM_KILLFOCUS:
219 | _lastImmOpenStatus = NativeMethods.ImmGetOpenStatus(DefaultImc);
220 | break;
221 | case NativeMethods.WM_IME_NOTIFY:
222 | IMENotify(wParam.ToInt32());
223 | if (!InputMethod.ShowOSImeWindow)
224 | return true;
225 | break;
226 | case NativeMethods.WM_IME_STARTCOMPOSITION:
227 | //Debug.WriteLine("NativeMethods.WM_IME_STARTCOMPOSITION");
228 | IMEStartComposion(lParam.ToInt32());
229 | // Force to not show composition window, `lParam64 &= ~ISC_SHOWUICOMPOSITIONWINDOW` don't work sometime.
230 | return true;
231 | case NativeMethods.WM_IME_COMPOSITION:
232 | //Debug.WriteLine("NativeMethods.WM_IME_COMPOSITION");
233 | IMEComposition(lParam.ToInt32());
234 | break;
235 | case NativeMethods.WM_IME_ENDCOMPOSITION:
236 | //Debug.WriteLine("NativeMethods.WM_IME_ENDCOMPOSITION");
237 | IMEEndComposition(lParam.ToInt32());
238 | if (!InputMethod.ShowOSImeWindow)
239 | return true;
240 | break;
241 | }
242 |
243 | return false;
244 | }
245 |
246 | private void IMENotify(int WParam)
247 | {
248 | switch (WParam)
249 | {
250 | case NativeMethods.IMN_OPENCANDIDATE:
251 | case NativeMethods.IMN_CHANGECANDIDATE:
252 | IMEChangeCandidate();
253 | break;
254 | case NativeMethods.IMN_CLOSECANDIDATE:
255 | InputMethod.ClearCandidates();
256 | break;
257 | default:
258 | break;
259 | }
260 | }
261 |
262 | private void IMEChangeCandidate()
263 | {
264 | if (TextServicesLoader.ServicesInstalled) // TSF is enabled
265 | {
266 | if (!TextStore.Current.SupportUIElement) // But active IME not support UIElement
267 | UpdateCandidates(); // We have to fetch candidate list here.
268 |
269 | return;
270 | }
271 |
272 | // Normal candidate list fetch in IMM32
273 | UpdateCandidates();
274 | // Send event on candidate updates
275 | InputMethod.OnTextComposition(this, new IMEString(_compositionStringHandler.Values, _compositionStringHandler.Count), _compositionCursorHandler.Value);
276 | }
277 |
278 | private unsafe void UpdateCandidates()
279 | {
280 | uint length = NativeMethods.ImmGetCandidateList(DefaultImc, 0, IntPtr.Zero, 0);
281 | if (length > 0)
282 | {
283 | IntPtr pointer = Marshal.AllocHGlobal((int)length);
284 | length = NativeMethods.ImmGetCandidateList(DefaultImc, 0, pointer, length);
285 | NativeMethods.CANDIDATELIST* cList = (NativeMethods.CANDIDATELIST*)pointer;
286 |
287 | var selection = (int)cList->dwSelection;
288 | var pageStart = (int)cList->dwPageStart;
289 | var pageSize = (int)cList->dwPageSize;
290 |
291 | selection -= pageStart;
292 |
293 | int i, j;
294 | for (i = pageStart, j = 0; i < cList->dwCount && j < pageSize; i++, j++)
295 | {
296 | int sOffset = Marshal.ReadInt32(pointer, 24 + 4 * i);
297 | InputMethod.CandidateList[j] = new IMEString(pointer + sOffset);
298 | }
299 |
300 | //Debug.WriteLine("IMM========IMM");
301 | //Debug.WriteLine("pageStart: {0}, pageSize: {1}, selection: {2}, candidates:", pageStart, pageSize, selection);
302 | //for (int k = 0; k < candidates.Length; k++)
303 | // Debug.WriteLine(" {2}{0}.{1}", k + 1, candidates[k], k == selection ? "*" : "");
304 | //Debug.WriteLine("IMM++++++++IMM");
305 |
306 | InputMethod.CandidatePageSize = pageSize;
307 | InputMethod.CandidateSelection = selection;
308 |
309 | Marshal.FreeHGlobal(pointer);
310 | }
311 | }
312 |
313 | private void ClearComposition()
314 | {
315 | _compositionStringHandler.Clear();
316 | }
317 |
318 | private void IMEStartComposion(int lParam)
319 | {
320 | InputMethod.OnTextCompositionStarted(this);
321 | ClearComposition();
322 | }
323 |
324 | private void IMEComposition(int lParam)
325 | {
326 | if (_compositionStringHandler.Update(lParam))
327 | {
328 | _compositionCursorHandler.Update();
329 |
330 | InputMethod.OnTextComposition(this, new IMEString(_compositionStringHandler.Values, _compositionStringHandler.Count), _compositionCursorHandler.Value);
331 | }
332 | }
333 |
334 | private void IMEEndComposition(int lParam)
335 | {
336 | InputMethod.ClearCandidates();
337 | ClearComposition();
338 |
339 | InputMethod.OnTextCompositionEnded(this);
340 | }
341 | }
342 | }
343 |
--------------------------------------------------------------------------------
/ImeSharp/ImmCompositionResultHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 | using System.Runtime.InteropServices;
4 | using ImeSharp.Native;
5 |
6 | namespace ImeSharp
7 | {
8 | internal abstract class ImmCompositionResultHandler
9 | {
10 | protected IntPtr _imeContext;
11 |
12 | public int Flag { get; private set; }
13 |
14 | internal ImmCompositionResultHandler(IntPtr imeContext, int flag)
15 | {
16 | this.Flag = flag;
17 | _imeContext = imeContext;
18 | }
19 |
20 | internal virtual void Update() { }
21 |
22 | internal bool Update(int lParam)
23 | {
24 | if ((lParam & Flag) == Flag)
25 | {
26 | Update();
27 | return true;
28 | }
29 | return false;
30 | }
31 | }
32 |
33 | internal class ImmCompositionStringHandler : ImmCompositionResultHandler
34 | {
35 | internal const int BufferSize = 1024;
36 | private byte[] _byteBuffer = new byte[BufferSize];
37 | private int _byteCount;
38 |
39 | private char[] _charBuffer = new char[BufferSize / 2];
40 | private int _charCount;
41 |
42 | public char[] Values { get { return _charBuffer; } }
43 | public int Count { get { return _charCount; } }
44 |
45 | public char this[int index]
46 | {
47 | get
48 | {
49 | if (index >= _charCount || index < 0)
50 | throw new ArgumentOutOfRangeException("index");
51 |
52 | return _charBuffer[index];
53 | }
54 | }
55 |
56 | internal ImmCompositionStringHandler(IntPtr imeContext, int flag) : base(imeContext, flag)
57 | {
58 | }
59 |
60 | public override string ToString()
61 | {
62 | if (_charCount <= 0)
63 | return string.Empty;
64 |
65 | return new string(_charBuffer, 0, _charCount);
66 | }
67 |
68 | internal void Clear()
69 | {
70 | Array.Clear(_byteBuffer, 0, _byteCount);
71 | _byteCount = 0;
72 |
73 | Array.Clear(_charBuffer, 0, _charCount);
74 | _charCount = 0;
75 | }
76 |
77 | internal override void Update()
78 | {
79 | _byteCount = NativeMethods.ImmGetCompositionString(_imeContext, Flag, IntPtr.Zero, 0);
80 | IntPtr pointer = Marshal.AllocHGlobal(_byteCount);
81 |
82 | try
83 | {
84 | Array.Clear(_byteBuffer, 0, _byteCount);
85 |
86 | if (_byteCount > 0)
87 | {
88 | NativeMethods.ImmGetCompositionString(_imeContext, Flag, pointer, _byteCount);
89 |
90 | Marshal.Copy(pointer, _byteBuffer, 0, _byteCount);
91 |
92 | Array.Clear(_charBuffer, 0, _charCount);
93 | _charCount = Encoding.Unicode.GetChars(_byteBuffer, 0, _byteCount, _charBuffer, 0);
94 | }
95 | }
96 | finally
97 | {
98 | Marshal.FreeHGlobal(pointer);
99 | }
100 | }
101 | }
102 |
103 | internal class ImmCompositionIntHandler : ImmCompositionResultHandler
104 | {
105 | public int Value { get; private set; }
106 |
107 | internal ImmCompositionIntHandler(IntPtr imeContext, int flag) : base(imeContext, flag) { }
108 |
109 | public override string ToString()
110 | {
111 | return Value.ToString();
112 | }
113 |
114 | internal override void Update()
115 | {
116 | Value = NativeMethods.ImmGetCompositionString(_imeContext, Flag, IntPtr.Zero, 0);
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/ImeSharp/InputMethod.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.Runtime.InteropServices;
6 | using ImeSharp.Native;
7 |
8 | namespace ImeSharp
9 | {
10 | public static class InputMethod
11 | {
12 | private static IntPtr _windowHandle;
13 | public static IntPtr WindowHandle { get { return _windowHandle; } }
14 |
15 | ///
16 | /// Option to force to disable TSF.
17 | ///
18 | public static bool TsfForceDisabled { get { return _tsfForceDisabled; } }
19 | private static bool _tsfForceDisabled;
20 |
21 | private static IntPtr _prevWndProc;
22 | private static NativeMethods.WndProcDelegate _wndProcDelegate;
23 |
24 | private static TextServicesContext _textServicesContext;
25 | internal static TextServicesContext TextServicesContext
26 | {
27 | get { return _textServicesContext; }
28 | set { _textServicesContext = value; }
29 | }
30 |
31 | private static TextStore _defaultTextStore;
32 | internal static TextStore DefaultTextStore
33 | {
34 | get { return _defaultTextStore; }
35 | set { _defaultTextStore = value; }
36 | }
37 |
38 | private static Imm32Manager _defaultImm32Manager;
39 | internal static Imm32Manager DefaultImm32Manager
40 | {
41 | get { return _defaultImm32Manager; }
42 | set { _defaultImm32Manager = value; }
43 | }
44 |
45 | private static bool _enabled;
46 | public static bool Enabled
47 | {
48 | get { return _enabled; }
49 | set
50 | {
51 | if (_enabled == value) return;
52 |
53 | _enabled = value;
54 |
55 | EnableOrDisableInputMethod(_enabled);
56 | }
57 | }
58 |
59 | internal static TsfSharp.Rect TextInputRect;
60 |
61 | ///
62 | /// Set the position of the candidate window rendered by the OS.
63 | /// Let the OS render the candidate window by set param "showOSImeWindow" to true on .
64 | ///
65 | public static void SetTextInputRect(int x, int y, int width, int height)
66 | {
67 | if (!_showOSImeWindow) return;
68 |
69 | TextInputRect.Left = x;
70 | TextInputRect.Top = y;
71 | TextInputRect.Right = x + width;
72 | TextInputRect.Bottom = y + height;
73 |
74 | if (Imm32Manager.ImmEnabled)
75 | Imm32Manager.Current.SetCandidateWindow(TextInputRect);
76 | }
77 |
78 | private static bool _showOSImeWindow = false;
79 |
80 | ///
81 | /// Return if let OS render IME Candidate window or not.
82 | ///
83 | public static bool ShowOSImeWindow { get { return _showOSImeWindow; } }
84 |
85 | public static int CandidatePageSize;
86 | public static int CandidateSelection;
87 | public static readonly IMEString[] CandidateList = new IMEString[16];
88 |
89 | internal static void ClearCandidates()
90 | {
91 | CandidatePageSize = 0;
92 | CandidateSelection = 0;
93 | }
94 |
95 | public static TextInputCallback TextInputCallback { get; set; }
96 | public static TextCompositionCallback TextCompositionCallback { get; set; }
97 |
98 | ///
99 | /// Initialize InputMethod with a Window Handle.
100 | /// Let the OS render the candidate window by set to true.
101 | ///
102 | public static void Initialize(IntPtr windowHandle, bool showOSImeWindow = true, bool tsfForceDisabled = false)
103 | {
104 | if (_windowHandle != IntPtr.Zero)
105 | throw new InvalidOperationException("InputMethod can only be initialized once!");
106 |
107 | _windowHandle = windowHandle;
108 | _showOSImeWindow = showOSImeWindow;
109 | _tsfForceDisabled = tsfForceDisabled;
110 |
111 | _wndProcDelegate = new NativeMethods.WndProcDelegate(WndProc);
112 | _prevWndProc = (IntPtr)NativeMethods.SetWindowLongPtr(_windowHandle, NativeMethods.GWL_WNDPROC,
113 | Marshal.GetFunctionPointerForDelegate(_wndProcDelegate));
114 | }
115 |
116 | internal static void OnTextInput(object sender, char character)
117 | {
118 | if (TextInputCallback != null)
119 | TextInputCallback(character);
120 | }
121 |
122 | // Some Chinese IME only send composition start event but no composition update event.
123 | // We need this to ensure candidate window position can be set in time.
124 | internal static void OnTextCompositionStarted(object sender)
125 | {
126 | if (TextCompositionCallback != null)
127 | TextCompositionCallback(string.Empty, 0);
128 | }
129 |
130 | // On text composition update.
131 | internal static void OnTextComposition(object sender, IMEString compositionText, int cursorPos)
132 | {
133 | if (compositionText.Count == 0) // Crash guard
134 | cursorPos = 0;
135 |
136 | if (cursorPos > compositionText.Count) // Another crash guard
137 | cursorPos = compositionText.Count;
138 |
139 | if (TextCompositionCallback != null)
140 | TextCompositionCallback(compositionText.ToString(), cursorPos);
141 | }
142 |
143 | internal static void OnTextCompositionEnded(object sender)
144 | {
145 | if (TextCompositionCallback != null)
146 | TextCompositionCallback(string.Empty, 0);
147 | }
148 |
149 | private static void EnableOrDisableInputMethod(bool bEnabled)
150 | {
151 | // InputMethod enable/disabled status was changed on the current focus Element.
152 | if (TextServicesLoader.ServicesInstalled)
153 | {
154 | if (bEnabled)
155 | TextServicesContext.Current.SetFocusOnDefaultTextStore();
156 | else
157 | TextServicesContext.Current.SetFocusOnEmptyDim();
158 | }
159 |
160 | // Under IMM32 enabled system, we associate default hIMC or null hIMC.
161 | if (Imm32Manager.ImmEnabled)
162 | {
163 | if (bEnabled)
164 | Imm32Manager.Current.Enable();
165 | else
166 | Imm32Manager.Current.Disable();
167 | }
168 | }
169 |
170 | private static IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
171 | {
172 | if (Imm32Manager.ImmEnabled)
173 | {
174 | if (Imm32Manager.Current.ProcessMessage(hWnd, msg, ref wParam, ref lParam))
175 | return IntPtr.Zero;
176 | }
177 |
178 | switch (msg)
179 | {
180 | case NativeMethods.WM_DESTROY:
181 | TextServicesContext.Current.Uninitialize(true);
182 | break;
183 | case NativeMethods.WM_CHAR:
184 | {
185 | if (InputMethod.Enabled)
186 | InputMethod.OnTextInput(null, (char)wParam.ToInt32());
187 |
188 | break;
189 | }
190 | }
191 |
192 | return NativeMethods.CallWindowProc(_prevWndProc, hWnd, msg, wParam, lParam);
193 | }
194 |
195 | ///
196 | /// Custom windows message pumping to fix frame stuck issue.
197 | /// Normally, you need call this method in handler.
198 | ///
199 | public static void PumpMessage()
200 | {
201 | if (!Enabled) return;
202 | if (!TextServicesLoader.ServicesInstalled) return;
203 |
204 | bool result;
205 | var msg = new NativeMethods.NativeMessage();
206 |
207 | do
208 | {
209 | result = NativeMethods.PeekMessage(out msg, _windowHandle, 0, 0, NativeMethods.PM_REMOVE);
210 |
211 | if (result)
212 | {
213 | NativeMethods.TranslateMessage(ref msg);
214 | NativeMethods.DispatchMessage(ref msg);
215 | }
216 | } while (result);
217 |
218 | NativeMethods.PostMessage(_windowHandle, NativeMethods.WM_NULL, IntPtr.Zero, IntPtr.Zero);
219 | }
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/ImeSharp/Native/NativeMethods.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 | using System.Runtime.InteropServices;
4 |
5 | namespace ImeSharp.Native
6 | {
7 | public partial class NativeMethods
8 | {
9 | #region Constants
10 |
11 | public const int S_OK = 0x00000000;
12 | public const int S_FALSE = 0x00000001;
13 | public const int E_FAIL = unchecked((int)0x80004005);
14 | public const int E_INVALIDARG = unchecked((int)0x80070057);
15 | public const int E_NOTIMPL = unchecked((int)0x80004001);
16 |
17 | public const int WM_KEYFIRST = 0x0100;
18 | public const int WM_KEYDOWN = 0x0100;
19 | public const int WM_KEYUP = 0x0101;
20 | public const int WM_CHAR = 0x0102;
21 | public const int WM_DEADCHAR = 0x0103;
22 | public const int WM_SYSKEYDOWN = 0x0104;
23 | public const int WM_SYSKEYUP = 0x0105;
24 | public const int WM_SYSCHAR = 0x0106;
25 | public const int WM_SYSDEADCHAR = 0x0107;
26 | public const int WM_UNICHAR = 0x0109;
27 | public const int WM_KEYLAST = 0x0109;
28 | public const int UNICODE_NOCHAR = 0xFFFF;
29 |
30 | public const int WM_NOTIFY = 0x004E;
31 | public const int WM_INPUTLANGCHANGEREQUEST = 0x0050;
32 | public const int WM_INPUTLANGCHANGE = 0x0051;
33 | public const int WM_TCARD = 0x0052;
34 | public const int WM_HELP = 0x0053;
35 | public const int WM_USERCHANGED = 0x0054;
36 | public const int WM_NOTIFYFORMAT = 0x0055;
37 |
38 | public const int GWL_WNDPROC = -4;
39 |
40 | public const int WM_ACTIVATE = 0x0006;
41 | // WM_ACTIVATE state values
42 | public const int WA_INACTIVE = 0;
43 | public const int WA_ACTIVE = 1;
44 | public const int WA_CLICKACTIVE = 2;
45 |
46 | public const int WM_SETFOCUS = 0x0007;
47 | public const int WM_KILLFOCUS = 0x0008;
48 |
49 | public const int WM_DESTROY = 0x0002;
50 | public const int WM_NULL = 0x0000;
51 | public const int WM_QUIT = 0x0012;
52 |
53 | public const int CLSCTX_INPROC_SERVER = 0x1;
54 |
55 | public const int PM_NOREMOVE = 0x0000;
56 | public const int PM_REMOVE = 0x0001;
57 | public const int PM_NOYIELD = 0x0002;
58 |
59 | #endregion Constants
60 |
61 | #region Structs
62 |
63 | [StructLayout(LayoutKind.Sequential)]
64 | public struct NativeMessage
65 | {
66 | public IntPtr handle;
67 | public uint msg;
68 | public IntPtr wParam;
69 | public IntPtr lParam;
70 | public uint time;
71 | public int ptX;
72 | public int ptY;
73 | }
74 |
75 | #endregion
76 |
77 | [DllImport("user32.dll")]
78 | public static extern int GetSystemMetrics(SM nIndex);
79 |
80 | // We have this wrapper because casting IntPtr to int may
81 | // generate OverflowException when one of high 32 bits is set.
82 | public static int IntPtrToInt32(IntPtr intPtr)
83 | {
84 | return unchecked((int)intPtr.ToInt64());
85 | }
86 |
87 | [DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Unicode)]
88 | public static extern IntPtr GetKeyboardLayout(int dwLayout);
89 |
90 | public delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
91 |
92 | [DllImport("user32.dll")]
93 | public static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
94 |
95 | // This static method is required because legacy OSes do not support
96 | public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong)
97 | {
98 | if (IntPtr.Size == 8)
99 | return SetWindowLongPtr64(hWnd, nIndex, dwNewLong);
100 | else
101 | return new IntPtr(SetWindowLong32(hWnd, nIndex, dwNewLong.ToInt32()));
102 | }
103 |
104 | [DllImport("user32.dll", EntryPoint = "SetWindowLong", CharSet = CharSet.Unicode)]
105 | private static extern int SetWindowLong32(IntPtr hWnd, int nIndex, int dwNewLong);
106 |
107 | [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr", CharSet = CharSet.Unicode)]
108 | private static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
109 |
110 | [DllImport("user32.dll", SetLastError = true)]
111 | public static extern bool GetWindowRect(IntPtr hwnd, out TsfSharp.Rect lpRect);
112 |
113 | [DllImport("user32", ExactSpelling = true, SetLastError = true)]
114 | public static extern int MapWindowPoints(IntPtr hWndFrom, IntPtr hWndTo, ref TsfSharp.Rect rect, [MarshalAs(UnmanagedType.U4)] int cPoints);
115 |
116 |
117 | [DllImport("user32.dll", CharSet = CharSet.Unicode)]
118 | public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, StringBuilder lParam);
119 |
120 | [DllImport("user32.dll", CharSet = CharSet.Unicode)]
121 | public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam);
122 |
123 | [DllImport("user32.dll", CharSet = CharSet.Unicode)]
124 | public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam);
125 |
126 | [DllImport("user32.dll", CharSet = CharSet.Unicode)]
127 | public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, ref IntPtr lParam);
128 |
129 | [DllImport("user32.dll", CharSet = CharSet.Unicode)]
130 | public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, IntPtr lParam);
131 |
132 | [DllImport("user32.dll", CharSet = CharSet.Unicode)]
133 | public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
134 |
135 | [DllImport("user32.dll")]
136 | public static extern bool TranslateMessage(ref NativeMessage lpMsg);
137 |
138 | [DllImport("user32.dll")]
139 | public static extern IntPtr DispatchMessage(ref NativeMessage lpmsg);
140 |
141 | [DllImport("User32.dll", CharSet = CharSet.Unicode)]
142 | public static extern bool PeekMessage(out NativeMessage msg, IntPtr hWnd, uint messageFilterMin, uint messageFilterMax, uint flags);
143 |
144 |
145 | [DllImport("ole32.dll", ExactSpelling = true, EntryPoint = "CoCreateInstance", PreserveSig = true)]
146 | public static extern int CoCreateInstance([In, MarshalAs(UnmanagedType.LPStruct)] Guid rclsid, IntPtr pUnkOuter, int dwClsContext, [In, MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv);
147 |
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/ImeSharp/Native/NativeMethodsIMM32.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.InteropServices;
3 |
4 | namespace ImeSharp.Native
5 | {
6 | public partial class NativeMethods
7 | {
8 | #region Constants
9 |
10 | public const int WM_IME_SETCONTEXT = 0x0281;
11 | public const int WM_IME_NOTIFY = 0x0282;
12 | public const int WM_IME_CONTROL = 0x0283;
13 | public const int WM_IME_COMPOSITIONFULL = 0x0284;
14 | public const int WM_IME_SELECT = 0x0285;
15 | public const int WM_IME_CHAR = 0x0286;
16 | public const int WM_IME_REQUEST = 0x0288;
17 | public const int WM_IME_KEYDOWN = 0x0290;
18 | public const int WM_IME_KEYUP = 0x0291;
19 | public const int WM_IME_STARTCOMPOSITION = 0x010D;
20 | public const int WM_IME_ENDCOMPOSITION = 0x010E;
21 | public const int WM_IME_COMPOSITION = 0x010F;
22 | public const int WM_IME_KEYLAST = 0x010F;
23 |
24 | // wParam of report message WM_IME_NOTIFY
25 | public const int IMN_CLOSESTATUSWINDOW = 0x0001;
26 | public const int IMN_OPENSTATUSWINDOW = 0x0002;
27 | public const int IMN_CHANGECANDIDATE = 0x0003;
28 | public const int IMN_CLOSECANDIDATE = 0x0004;
29 | public const int IMN_OPENCANDIDATE = 0x0005;
30 | public const int IMN_SETCONVERSIONMODE = 0x0006;
31 | public const int IMN_SETSENTENCEMODE = 0x0007;
32 | public const int IMN_SETOPENSTATUS = 0x0008;
33 | public const int IMN_SETCANDIDATEPOS = 0x0009;
34 | public const int IMN_SETCOMPOSITIONFONT = 0x000A;
35 | public const int IMN_SETCOMPOSITIONWINDOW = 0x000B;
36 | public const int IMN_SETSTATUSWINDOWPOS = 0x000C;
37 | public const int IMN_GUIDELINE = 0x000D;
38 | public const int IMN_PRIVATE = 0x000E;
39 |
40 | // wParam of report message WM_IME_REQUEST
41 | public const int IMR_COMPOSITIONWINDOW = 0x0001;
42 | public const int IMR_CANDIDATEWINDOW = 0x0002;
43 | public const int IMR_COMPOSITIONFONT = 0x0003;
44 | public const int IMR_RECONVERTSTRING = 0x0004;
45 | public const int IMR_CONFIRMRECONVERTSTRING = 0x0005;
46 | public const int IMR_QUERYCHARPOSITION = 0x0006;
47 | public const int IMR_DOCUMENTFEED = 0x0007;
48 |
49 | // parameter of ImmGetCompositionString
50 | public const int GCS_COMPREADSTR = 0x0001;
51 | public const int GCS_COMPREADATTR = 0x0002;
52 | public const int GCS_COMPREADCLAUSE = 0x0004;
53 | public const int GCS_COMPSTR = 0x0008;
54 | public const int GCS_COMPATTR = 0x0010;
55 | public const int GCS_COMPCLAUSE = 0x0020;
56 | public const int GCS_CURSORPOS = 0x0080;
57 | public const int GCS_DELTASTART = 0x0100;
58 | public const int GCS_RESULTREADSTR = 0x0200;
59 | public const int GCS_RESULTREADCLAUSE = 0x0400;
60 | public const int GCS_RESULTSTR = 0x0800;
61 | public const int GCS_RESULTCLAUSE = 0x1000;
62 |
63 | public const int GCS_COMP = (GCS_COMPSTR | GCS_COMPATTR | GCS_COMPCLAUSE);
64 | public const int GCS_COMPREAD = (GCS_COMPREADSTR | GCS_COMPREADATTR | GCS_COMPREADCLAUSE);
65 | public const int GCS_RESULT = (GCS_RESULTSTR | GCS_RESULTCLAUSE);
66 | public const int GCS_RESULTREAD = (GCS_RESULTREADSTR | GCS_RESULTREADCLAUSE);
67 |
68 | public const int CFS_CANDIDATEPOS = 0x0040;
69 | public const int CFS_POINT = 0x0002;
70 | public const int CFS_EXCLUDE = 0x0080;
71 |
72 | // lParam for WM_IME_SETCONTEXT
73 | public const long ISC_SHOWUICANDIDATEWINDOW = 0x00000001;
74 | public const long ISC_SHOWUICOMPOSITIONWINDOW = 0x80000000;
75 | public const long ISC_SHOWUIGUIDELINE = 0x40000000;
76 | public const long ISC_SHOWUIALLCANDIDATEWINDOW = 0x0000000F;
77 | public const long ISC_SHOWUIALL = 0xC000000F;
78 |
79 | #endregion Constants
80 |
81 | [StructLayout(LayoutKind.Sequential)]
82 | public unsafe struct CANDIDATELIST
83 | {
84 | public uint dwSize;
85 | public uint dwStyle;
86 | public uint dwCount;
87 | public uint dwSelection;
88 | public uint dwPageStart;
89 | public uint dwPageSize;
90 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1, ArraySubType = UnmanagedType.U4)]
91 | public fixed uint dwOffset[1];
92 | }
93 |
94 | // CANDIDATEFORM structures
95 | [StructLayout(LayoutKind.Sequential)]
96 | public struct CANDIDATEFORM
97 | {
98 | public int dwIndex;
99 | public int dwStyle;
100 | public TsfSharp.Point ptCurrentPos;
101 | public TsfSharp.Rect rcArea;
102 | }
103 |
104 | // COMPOSITIONFORM structures
105 | [StructLayout(LayoutKind.Sequential)]
106 | public struct COMPOSITIONFORM
107 | {
108 | public int dwStyle;
109 | public TsfSharp.Point ptCurrentPos;
110 | public TsfSharp.Rect rcArea;
111 | }
112 |
113 | [DllImport("imm32.dll", SetLastError = true)]
114 | public static extern IntPtr ImmCreateContext();
115 |
116 | [DllImport("imm32.dll", SetLastError = true)]
117 | public static extern bool ImmDestroyContext(IntPtr hIMC);
118 |
119 | [DllImport("imm32.dll", SetLastError = true)]
120 | public static extern IntPtr ImmAssociateContext(IntPtr hWnd, IntPtr hIMC);
121 |
122 | [DllImport("imm32.dll", SetLastError = true)]
123 | public static extern IntPtr ImmReleaseContext(IntPtr hWnd, IntPtr hIMC);
124 |
125 | [DllImport("imm32.dll", CharSet = CharSet.Unicode)]
126 | public static extern uint ImmGetCandidateList(IntPtr hIMC, uint deIndex, IntPtr candidateList, uint dwBufLen);
127 |
128 | [DllImport("imm32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
129 | public static extern int ImmGetCompositionString(IntPtr hIMC, int CompositionStringFlag, IntPtr buffer, int bufferLength);
130 |
131 | [DllImport("imm32.dll", SetLastError = true)]
132 | public static extern IntPtr ImmGetContext(IntPtr hWnd);
133 |
134 | [DllImport("Imm32.dll", SetLastError = true)]
135 | public static extern bool ImmGetOpenStatus(IntPtr hIMC);
136 |
137 | [DllImport("Imm32.dll", SetLastError = true)]
138 | public static extern bool ImmSetOpenStatus(IntPtr hIMC, bool open);
139 |
140 | [DllImport("imm32.dll", SetLastError = true)]
141 | public static extern bool ImmSetCandidateWindow(IntPtr hIMC, ref CANDIDATEFORM candidateForm);
142 |
143 | [DllImport("imm32.dll", SetLastError = true)]
144 | public static extern int ImmSetCompositionWindow(IntPtr hIMC, ref COMPOSITIONFORM compForm);
145 |
146 |
147 | [DllImport("user32.dll", SetLastError = true)]
148 | public static extern bool CreateCaret(IntPtr hWnd, IntPtr hBitmap, int nWidth, int nHeight);
149 |
150 | [DllImport("user32.dll", SetLastError = true)]
151 | public static extern bool DestroyCaret();
152 |
153 | [DllImport("user32.dll", SetLastError = true)]
154 | public static extern bool SetCaretPos(int x, int y);
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/ImeSharp/Native/NativeValues.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ImeSharp.Native
4 | {
5 | ///
6 | /// SystemMetrics. SM_*
7 | ///
8 | public enum SM
9 | {
10 | CXSCREEN = 0,
11 | CYSCREEN = 1,
12 | CXVSCROLL = 2,
13 | CYHSCROLL = 3,
14 | CYCAPTION = 4,
15 | CXBORDER = 5,
16 | CYBORDER = 6,
17 | CXFIXEDFRAME = 7,
18 | CYFIXEDFRAME = 8,
19 | CYVTHUMB = 9,
20 | CXHTHUMB = 10,
21 | CXICON = 11,
22 | CYICON = 12,
23 | CXCURSOR = 13,
24 | CYCURSOR = 14,
25 | CYMENU = 15,
26 | CXFULLSCREEN = 16,
27 | CYFULLSCREEN = 17,
28 | CYKANJIWINDOW = 18,
29 | MOUSEPRESENT = 19,
30 | CYVSCROLL = 20,
31 | CXHSCROLL = 21,
32 | DEBUG = 22,
33 | SWAPBUTTON = 23,
34 | CXMIN = 28,
35 | CYMIN = 29,
36 | CXSIZE = 30,
37 | CYSIZE = 31,
38 | CXFRAME = 32,
39 | CXSIZEFRAME = CXFRAME,
40 | CYFRAME = 33,
41 | CYSIZEFRAME = CYFRAME,
42 | CXMINTRACK = 34,
43 | CYMINTRACK = 35,
44 | CXDOUBLECLK = 36,
45 | CYDOUBLECLK = 37,
46 | CXICONSPACING = 38,
47 | CYICONSPACING = 39,
48 | MENUDROPALIGNMENT = 40,
49 | PENWINDOWS = 41,
50 | DBCSENABLED = 42,
51 | CMOUSEBUTTONS = 43,
52 | SECURE = 44,
53 | CXEDGE = 45,
54 | CYEDGE = 46,
55 | CXMINSPACING = 47,
56 | CYMINSPACING = 48,
57 | CXSMICON = 49,
58 | CYSMICON = 50,
59 | CYSMCAPTION = 51,
60 | CXSMSIZE = 52,
61 | CYSMSIZE = 53,
62 | CXMENUSIZE = 54,
63 | CYMENUSIZE = 55,
64 | ARRANGE = 56,
65 | CXMINIMIZED = 57,
66 | CYMINIMIZED = 58,
67 | CXMAXTRACK = 59,
68 | CYMAXTRACK = 60,
69 | CXMAXIMIZED = 61,
70 | CYMAXIMIZED = 62,
71 | NETWORK = 63,
72 | CLEANBOOT = 67,
73 | CXDRAG = 68,
74 | CYDRAG = 69,
75 | SHOWSOUNDS = 70,
76 | CXMENUCHECK = 71,
77 | CYMENUCHECK = 72,
78 | SLOWMACHINE = 73,
79 | MIDEASTENABLED = 74,
80 | MOUSEWHEELPRESENT = 75,
81 | XVIRTUALSCREEN = 76,
82 | YVIRTUALSCREEN = 77,
83 | CXVIRTUALSCREEN = 78,
84 | CYVIRTUALSCREEN = 79,
85 | CMONITORS = 80,
86 | SAMEDISPLAYFORMAT = 81,
87 | IMMENABLED = 82,
88 | CXFOCUSBORDER = 83,
89 | CYFOCUSBORDER = 84,
90 | TABLETPC = 86,
91 | MEDIACENTER = 87,
92 | REMOTESESSION = 0x1000,
93 | REMOTECONTROL = 0x2001,
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/ImeSharp/SafeSystemMetrics.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using ImeSharp.Native;
3 |
4 | namespace ImeSharp
5 | {
6 | ///
7 | /// Contains properties that are queries into the system's various settings.
8 | ///
9 | internal sealed class SafeSystemMetrics
10 | {
11 |
12 | private SafeSystemMetrics()
13 | {
14 | }
15 |
16 | ///
17 | /// Maps to SM_CXDOUBLECLK
18 | ///
19 | public static int DoubleClickDeltaX
20 | {
21 | get { return NativeMethods.GetSystemMetrics(SM.CXDOUBLECLK); }
22 | }
23 |
24 | ///
25 | /// Maps to SM_CYDOUBLECLK
26 | ///
27 | public static int DoubleClickDeltaY
28 | {
29 | get { return NativeMethods.GetSystemMetrics(SM.CYDOUBLECLK); }
30 | }
31 |
32 |
33 | ///
34 | /// Maps to SM_CXDRAG
35 | ///
36 | public static int DragDeltaX
37 | {
38 | get { return NativeMethods.GetSystemMetrics(SM.CXDRAG); }
39 | }
40 |
41 | ///
42 | /// Maps to SM_CYDRAG
43 | ///
44 | public static int DragDeltaY
45 | {
46 | get { return NativeMethods.GetSystemMetrics(SM.CYDRAG); }
47 | }
48 |
49 | ///
50 | /// Is an IMM enabled ? Maps to SM_IMMENABLED
51 | ///
52 | public static bool IsImmEnabled
53 | {
54 | get { return (NativeMethods.GetSystemMetrics(SM.IMMENABLED) != 0); }
55 | }
56 |
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/ImeSharp/TextInputCallbacks.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ImeSharp
4 | {
5 | public delegate void TextInputCallback(char character);
6 | public delegate void TextCompositionCallback(string compositionText, int cursorPosition);
7 | }
8 |
--------------------------------------------------------------------------------
/ImeSharp/TextServicesContext.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.InteropServices;
3 | using System.Threading;
4 | using System.Diagnostics;
5 | using ImeSharp.Native;
6 | using TsfSharp;
7 |
8 | namespace ImeSharp
9 | {
10 | //------------------------------------------------------
11 | //
12 | // TextServicesContext class
13 | //
14 | //------------------------------------------------------
15 |
16 | ///
17 | /// This class manages the ITfThreadMgr, EmptyDim and the reference to
18 | /// the default TextStore.
19 | ///
20 | ///
21 | ///
22 | internal class TextServicesContext
23 | {
24 | public const int TF_POPF_ALL = 0x0001;
25 | public const int TF_INVALID_COOKIE = -1;
26 | public static readonly Guid IID_ITfUIElementSink = new Guid(0xea1ea136, 0x19df, 0x11d7, 0xa6, 0xd2, 0x00, 0x06, 0x5b, 0x84, 0x43, 0x5c);
27 | public static readonly Guid IID_ITfTextEditSink = new Guid(0x8127d409, 0xccd3, 0x4683, 0x96, 0x7a, 0xb4, 0x3d, 0x5b, 0x48, 0x2b, 0xf7);
28 |
29 |
30 | public static TextServicesContext Current
31 | {
32 | get
33 | {
34 | if (InputMethod.TextServicesContext == null)
35 | InputMethod.TextServicesContext = new TextServicesContext();
36 |
37 | return InputMethod.TextServicesContext;
38 | }
39 | }
40 |
41 | //------------------------------------------------------
42 | //
43 | // Constructors
44 | //
45 | //------------------------------------------------------
46 |
47 | #region Constructors
48 |
49 | ///
50 | /// Instantiates a TextServicesContext.
51 | ///
52 | private TextServicesContext()
53 | {
54 | if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA)
55 | Debug.WriteLine("CRASH: ImeSharp won't work on MTA thread!!!");
56 | }
57 |
58 | #endregion Constructors
59 |
60 | //------------------------------------------------------
61 | //
62 | // public Methods
63 | //
64 | //------------------------------------------------------
65 |
66 | #region public Methods
67 |
68 | ///
69 | /// Releases all unmanaged resources allocated by the
70 | /// TextServicesContext.
71 | ///
72 | ///
73 | /// if appDomainShutdown == false, this method must be called on the
74 | /// Dispatcher thread. Otherwise, the caller is an AppDomain.Shutdown
75 | /// listener, and is calling from a worker thread.
76 | ///
77 | public void Uninitialize(bool appDomainShutdown)
78 | {
79 | // Unregister DefaultTextStore.
80 | if (_defaultTextStore != null)
81 | {
82 | UnadviseSinks();
83 | if (_defaultTextStore.DocumentManager != null)
84 | {
85 | _defaultTextStore.DocumentManager.Pop(TF_POPF_ALL);
86 | _defaultTextStore.DocumentManager.Dispose();
87 | _defaultTextStore.DocumentManager = null;
88 | }
89 |
90 | _defaultTextStore = null;
91 | }
92 |
93 | // Free up any remaining textstores.
94 | if (_istimactivated == true)
95 | {
96 | // Shut down the thread manager when the last TextStore goes away.
97 | // On XP, if we're called on a worker thread (during AppDomain shutdown)
98 | // we can't call call any methods on _threadManager. The problem is
99 | // that there's no proxy registered for ITfThreadMgr on OS versions
100 | // previous to Vista. Not calling Deactivate will leak the IMEs, but
101 | // in practice (1) they're singletons, so it's not unbounded; and (2)
102 | // most applications will share the thread with other AppDomains that
103 | // have a UI, in which case the IME won't be released until the process
104 | // shuts down in any case. In theory we could also work around this
105 | // problem by creating our own XP proxy/stub implementation, which would
106 | // be added to WPF setup....
107 | if (!appDomainShutdown || System.Environment.OSVersion.Version.Major >= 6)
108 | {
109 | _threadManager.Deactivate();
110 | }
111 | _istimactivated = false;
112 | }
113 |
114 | // Release the empty dim.
115 | if (_dimEmpty != null)
116 | {
117 | if (_dimEmpty != null)
118 | {
119 | _dimEmpty.Dispose();
120 | }
121 | _dimEmpty = null;
122 | }
123 |
124 | // Release the ThreadManager.
125 | // We don't do this in UnregisterTextStore because someone may have
126 | // called get_ThreadManager after the last TextStore was unregistered.
127 | if (_threadManager != null)
128 | {
129 | if (_threadManager != null)
130 | {
131 | _threadManager.Dispose();
132 | }
133 | _threadManager = null;
134 | }
135 | }
136 |
137 | // Called by framework's TextStore class. This method registers a
138 | // document with TSF. The TextServicesContext must maintain this list
139 | // to ensure all native resources are released after gc or uninitialization.
140 | public void RegisterTextStore(TextStore defaultTextStore)
141 | {
142 | _defaultTextStore = defaultTextStore;
143 |
144 | ITfThreadMgrEx threadManager = ThreadManager;
145 |
146 | if (threadManager != null)
147 | {
148 | ITfDocumentMgr doc;
149 | int editCookie = TF_INVALID_COOKIE;
150 |
151 | // Activate TSF on this thread if this is the first TextStore.
152 | if (_istimactivated == false)
153 | {
154 | //temp variable created to retrieve the value
155 | // which is then stored in the critical data.
156 | if (InputMethod.ShowOSImeWindow)
157 | _clientId = threadManager.Activate();
158 | else
159 | _clientId = threadManager.ActivateEx(TfTmaeFlags.Uielementenabledonly);
160 |
161 | _istimactivated = true;
162 | }
163 |
164 | // Create a TSF document.
165 | doc = threadManager.CreateDocumentMgr();
166 | _defaultTextStore.DocumentManager = doc;
167 |
168 | doc.CreateContext(_clientId, 0 /* flags */, _defaultTextStore, out _editContext, out editCookie);
169 | _defaultTextStore.EditCookie = editCookie;
170 | _contextOwnerServices = _editContext.QueryInterface();
171 |
172 | doc.Push(_editContext);
173 |
174 | AdviseSinks();
175 | }
176 | }
177 |
178 |
179 | public void SetFocusOnDefaultTextStore()
180 | {
181 | SetFocusOnDim(TextStore.Current.DocumentManager);
182 | }
183 |
184 | public void SetFocusOnEmptyDim()
185 | {
186 | SetFocusOnDim(EmptyDocumentManager);
187 | }
188 |
189 |
190 | #endregion public Methods
191 |
192 | //------------------------------------------------------
193 | //
194 | // public Properties
195 | //
196 | //------------------------------------------------------
197 |
198 | ///
199 | /// The default ITfThreadMgrEx object.
200 | ///
201 | public ITfThreadMgrEx ThreadManager
202 | {
203 | // The ITfThreadMgr for this thread.
204 | get
205 | {
206 | if (_threadManager == null)
207 | {
208 | ITfThreadMgr threadMgr = null;
209 | try
210 | {
211 | // This might fail in CoreRT
212 | threadMgr = Tsf.GetThreadMgr();
213 | }
214 | catch (SharpGen.Runtime.SharpGenException)
215 | {
216 | threadMgr = null;
217 | }
218 |
219 | // Dispose previous ITfThreadMgr in case something weird happens
220 | if (threadMgr != null)
221 | {
222 | if (threadMgr.IsThreadFocus)
223 | threadMgr.Deactivate();
224 | threadMgr.Dispose();
225 | }
226 |
227 | _threadManager = TextServicesLoader.Load();
228 |
229 | _uiElementMgr = _threadManager.QueryInterface();
230 | }
231 |
232 | return _threadManager;
233 | }
234 | }
235 |
236 | ///
237 | /// Return the created ITfContext object.
238 | ///
239 | public ITfContext EditContext
240 | {
241 | get { return _editContext; }
242 | }
243 |
244 | ///
245 | /// Return the created ITfUIElementMgr object.
246 | ///
247 | public ITfUIElementMgr UIElementMgr
248 | {
249 | get { return _uiElementMgr; }
250 | }
251 |
252 | ///
253 | /// Return the created ITfContextOwnerServices object.
254 | ///
255 | public ITfContextOwnerServices ContextOwnerServices
256 | {
257 | get { return _contextOwnerServices; }
258 | }
259 |
260 | //------------------------------------------------------
261 | //
262 | // public Events
263 | //
264 | //------------------------------------------------------
265 |
266 | //------------------------------------------------------
267 | //
268 | // Private Methods
269 | //
270 | //------------------------------------------------------
271 |
272 | private void SetFocusOnDim(ITfDocumentMgr dim)
273 | {
274 | ITfThreadMgrEx threadmgr = ThreadManager;
275 |
276 | if (threadmgr != null)
277 | {
278 | ITfDocumentMgr prevDocMgr = threadmgr.AssociateFocus(InputMethod.WindowHandle, dim);
279 | }
280 | }
281 |
282 | private void AdviseSinks()
283 | {
284 | var source = _uiElementMgr.QueryInterface();
285 | var guid = IID_ITfUIElementSink;
286 | int sinkCookie = source.AdviseSink(guid, _defaultTextStore);
287 | _defaultTextStore.UIElementSinkCookie = sinkCookie;
288 | source.Dispose();
289 |
290 | source = _editContext.QueryInterface();
291 | guid = IID_ITfTextEditSink;
292 | sinkCookie = source.AdviseSink(guid, _defaultTextStore);
293 | _defaultTextStore.TextEditSinkCookie = sinkCookie;
294 | source.Dispose();
295 | }
296 |
297 | private void UnadviseSinks()
298 | {
299 | var source = _uiElementMgr.QueryInterface();
300 |
301 | if (_defaultTextStore.UIElementSinkCookie != TF_INVALID_COOKIE)
302 | {
303 | source.UnadviseSink(_defaultTextStore.UIElementSinkCookie);
304 | _defaultTextStore.UIElementSinkCookie = TF_INVALID_COOKIE;
305 | }
306 | source.Dispose();
307 |
308 | source = _editContext.QueryInterface();
309 | if (_defaultTextStore.TextEditSinkCookie != TF_INVALID_COOKIE)
310 | {
311 | source.UnadviseSink(_defaultTextStore.TextEditSinkCookie);
312 | _defaultTextStore.TextEditSinkCookie = TF_INVALID_COOKIE;
313 | }
314 | source.Dispose();
315 | }
316 |
317 | //------------------------------------------------------
318 | //
319 | // Private Properties
320 | //
321 | //------------------------------------------------------
322 |
323 | // Create an empty dim on demand.
324 | private ITfDocumentMgr EmptyDocumentManager
325 | {
326 | get
327 | {
328 | if (_dimEmpty == null)
329 | {
330 | ITfThreadMgrEx threadManager = ThreadManager;
331 | if (threadManager == null)
332 | {
333 | return null;
334 | }
335 |
336 | ITfDocumentMgr dimEmptyTemp;
337 | // Create a TSF document.
338 | dimEmptyTemp = threadManager.CreateDocumentMgr();
339 | _dimEmpty = dimEmptyTemp;
340 | }
341 | return _dimEmpty;
342 | }
343 | }
344 |
345 |
346 | //------------------------------------------------------
347 | //
348 | // Private Fields
349 | //
350 | //------------------------------------------------------
351 |
352 | #region Private Fields
353 |
354 | private TextStore _defaultTextStore;
355 |
356 | private ITfContext _editContext;
357 | private ITfUIElementMgr _uiElementMgr;
358 | private ITfContextOwnerServices _contextOwnerServices;
359 |
360 | // This is true if thread manager is activated.
361 | private bool _istimactivated;
362 |
363 | // The root TSF object, created on demand.
364 | private ITfThreadMgrEx _threadManager;
365 |
366 | // TSF ClientId from Activate call.
367 | private int _clientId;
368 |
369 | // The empty dim for this thread. Created on demand.
370 | private ITfDocumentMgr _dimEmpty;
371 |
372 | #endregion Private Fields
373 | }
374 | }
375 |
--------------------------------------------------------------------------------
/ImeSharp/TextServicesLoader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.InteropServices;
3 | using System.Diagnostics;
4 | using System.Threading;
5 | using Microsoft.Win32;
6 | using ImeSharp.Native;
7 | using TsfSharp;
8 |
9 | namespace ImeSharp
10 | {
11 | // Creates ITfThreadMgr instances, the root object of the Text Services
12 | // Framework.
13 | internal class TextServicesLoader
14 | {
15 | public static readonly Guid CLSID_TF_ThreadMgr = new Guid("529a9e6b-6587-4f23-ab9e-9c7d683e3c50");
16 | public static readonly Guid IID_ITfThreadMgr = new Guid("aa80e801-2021-11d2-93e0-0060b067b86e");
17 | public static readonly Guid IID_ITfThreadMgrEx = new Guid("3e90ade3-7594-4cb0-bb58-69628f5f458c");
18 | public static readonly Guid IID_ITfThreadMgr2 = new Guid("0AB198EF-6477-4EE8-8812-6780EDB82D5E");
19 |
20 | //------------------------------------------------------
21 | //
22 | // Constructors
23 | //
24 | //------------------------------------------------------
25 |
26 | #region Constructors
27 |
28 | // Private ctor to prevent anyone from instantiating this static class.
29 | private TextServicesLoader() { }
30 |
31 | #endregion Constructors
32 |
33 | //------------------------------------------------------
34 | //
35 | // public Properties
36 | //
37 | //------------------------------------------------------
38 |
39 | #region public Properties
40 |
41 | ///
42 | /// Loads an instance of the Text Services Framework.
43 | ///
44 | ///
45 | /// May return null if no text services are available.
46 | ///
47 | public static ITfThreadMgrEx Load()
48 | {
49 | if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA)
50 | Debug.WriteLine("CRASH: ImeSharp won't work on MTA thread!!!");
51 |
52 | if (ServicesInstalled)
53 | {
54 | // NB: a COMException here means something went wrong initialzing Cicero.
55 | // Cicero will throw an exception if it doesn't think it should have been
56 | // loaded (no TIPs to run), you can check that in msctf.dll's NoTipsInstalled
57 | // which lives in nt\windows\advcore\ctf\lib\immxutil.cpp. If that's the
58 | // problem, ServicesInstalled is out of sync with Cicero's thinking.
59 | IntPtr ret;
60 | var hr = NativeMethods.CoCreateInstance(CLSID_TF_ThreadMgr,
61 | IntPtr.Zero,
62 | NativeMethods.CLSCTX_INPROC_SERVER,
63 | IID_ITfThreadMgrEx, out ret);
64 |
65 | if (hr == NativeMethods.S_OK)
66 | return new ITfThreadMgrEx(ret);
67 | }
68 |
69 | return null;
70 | }
71 |
72 | ///
73 | /// return true if current OS version is Windows 7 or below.
74 | ///
75 | public static bool IsWindows7OrBelow()
76 | {
77 | if (Environment.OSVersion.Version.Major <= 5)
78 | return true;
79 |
80 | if (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor <= 1)
81 | return true;
82 |
83 | return false;
84 | }
85 |
86 | ///
87 | /// Informs the caller if text services are installed for the current user.
88 | ///
89 | ///
90 | /// true if one or more text services are installed for the current user, otherwise false.
91 | ///
92 | ///
93 | /// If this method returns false, TextServicesLoader.Load is guarenteed to return null.
94 | /// Callers can use this information to avoid overhead that would otherwise be
95 | /// required to support text services.
96 | ///
97 | public static bool ServicesInstalled
98 | {
99 | get
100 | {
101 | if (InputMethod.TsfForceDisabled) return false;
102 |
103 | lock (s_servicesInstalledLock)
104 | {
105 | if (s_servicesInstalled == InstallState.Unknown)
106 | {
107 | s_servicesInstalled = TIPsWantToRun() ? InstallState.Installed : InstallState.NotInstalled;
108 | }
109 | }
110 |
111 | return (s_servicesInstalled == InstallState.Installed);
112 | }
113 | }
114 |
115 | #endregion public Properties
116 |
117 | //------------------------------------------------------
118 | //
119 | // public Events
120 | //
121 | //------------------------------------------------------
122 |
123 | //------------------------------------------------------
124 | //
125 | // Private Methods
126 | //
127 | //------------------------------------------------------
128 |
129 | #region Private Methods
130 |
131 | //
132 | // This method tries to stop Avalon from loading Cicero when there are no TIPs to run.
133 | // The perf tradeoff is a typically small number of registry checks versus loading and
134 | // initializing cicero.
135 | //
136 | // The Algorithm:
137 | //
138 | // Do a quick check vs. the global disable flag, return false if it is set.
139 | // For each key under HKLM\SOFTWARE\Microsoft\CTF\TIP (a TIP or category clsid)
140 | // If the the key has a LanguageProfile subkey (it's a TIP clsid)
141 | // Iterate under the matching TIP entry in HKCU.
142 | // For each key under the LanguageProfile (a particular LANGID)
143 | // For each key under the LANGID (an assembly GUID)
144 | // Try to read the Enable value.
145 | // If the value is set non-zero, then stop all processing and return true.
146 | // If the value is set zero, continue.
147 | // If the value does not exist, continue (default is disabled).
148 | // If any Enable values were found under HKCU for the TIP, then stop all processing and return false.
149 | // Else, no Enable values have been found thus far and we keep going to investigate HKLM.
150 | // Iterate under the TIP entry in HKLM.
151 | // For each key under the LanguageProfile (a particular LANGID)
152 | // For each key under the LANGID (an assembly GUID)
153 | // Try to read the Enable value.
154 | // If the value is set non-zero, then stop all processing and return true.
155 | // If the value does not exist, then stop all processing and return true (default is enabled).
156 | // If the value is set zero, continue.
157 | // If we finish iterating all entries under HKLM without returning true, return false.
158 | //
159 |
160 | private static bool TIPsWantToRun()
161 | {
162 | object obj;
163 | RegistryKey key;
164 | bool tipsWantToRun = false;
165 |
166 | key = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\CTF", false);
167 |
168 | // Is cicero disabled completely for the current user?
169 | if (key != null)
170 | {
171 | obj = key.GetValue("Disable Thread Input Manager");
172 |
173 | if (obj is int && (int)obj != 0)
174 | return false;
175 | }
176 |
177 | // Loop through all the TIP entries for machine and current user.
178 | tipsWantToRun = IterateSubKeys(Registry.LocalMachine, "SOFTWARE\\Microsoft\\CTF\\TIP", new IterateHandler(SingleTIPWantsToRun), true) == EnableState.Enabled;
179 |
180 | return tipsWantToRun;
181 | }
182 |
183 | // Returns EnableState.Enabled if one or more TIPs are installed and
184 | // enabled for the current user.
185 | private static EnableState SingleTIPWantsToRun(RegistryKey keyLocalMachine, string subKeyName, bool localMachine)
186 | {
187 | EnableState result;
188 |
189 | if (subKeyName.Length != CLSIDLength)
190 | return EnableState.Disabled;
191 |
192 | // We want subkey\LanguageProfile key.
193 | // Loop through all the langid entries for TIP.
194 |
195 | // First, check current user.
196 | result = IterateSubKeys(Registry.CurrentUser, "SOFTWARE\\Microsoft\\CTF\\TIP\\" + subKeyName + "\\LanguageProfile", new IterateHandler(IsLangidEnabled), false);
197 |
198 | // Any explicit value short circuits the process.
199 | // Otherwise check local machine.
200 | if (result == EnableState.None || result == EnableState.Error)
201 | {
202 | result = IterateSubKeys(keyLocalMachine, subKeyName + "\\LanguageProfile", new IterateHandler(IsLangidEnabled), true);
203 |
204 | if (result == EnableState.None)
205 | {
206 | result = EnableState.Enabled;
207 | }
208 | }
209 |
210 | return result;
211 | }
212 |
213 | // Returns EnableState.Enabled if the supplied subkey is a valid LANGID key with enabled
214 | // cicero assembly.
215 | private static EnableState IsLangidEnabled(RegistryKey key, string subKeyName, bool localMachine)
216 | {
217 | if (subKeyName.Length != LANGIDLength)
218 | return EnableState.Error;
219 |
220 | // Loop through all the assembly entries for the langid
221 | return IterateSubKeys(key, subKeyName, new IterateHandler(IsAssemblyEnabled), localMachine);
222 | }
223 |
224 | // Returns EnableState.Enabled if the supplied assembly key is enabled.
225 | private static EnableState IsAssemblyEnabled(RegistryKey key, string subKeyName, bool localMachine)
226 | {
227 | RegistryKey subKey;
228 | object obj;
229 |
230 | if (subKeyName.Length != CLSIDLength)
231 | return EnableState.Error;
232 |
233 | // Open the local machine assembly key.
234 | subKey = key.OpenSubKey(subKeyName);
235 |
236 | if (subKey == null)
237 | return EnableState.Error;
238 |
239 | // Try to read the "Enable" value.
240 | obj = subKey.GetValue("Enable");
241 |
242 | if (obj is int)
243 | {
244 | return ((int)obj == 0) ? EnableState.Disabled : EnableState.Enabled;
245 | }
246 |
247 | return EnableState.None;
248 | }
249 |
250 | // Calls the supplied delegate on each of the children of keyBase.
251 | private static EnableState IterateSubKeys(RegistryKey keyBase, string subKey, IterateHandler handler, bool localMachine)
252 | {
253 | RegistryKey key;
254 | string[] subKeyNames;
255 | EnableState state;
256 |
257 | key = keyBase.OpenSubKey(subKey, false);
258 |
259 | if (key == null)
260 | return EnableState.Error;
261 |
262 | subKeyNames = key.GetSubKeyNames();
263 | state = EnableState.Error;
264 |
265 | foreach (string name in subKeyNames)
266 | {
267 | switch (handler(key, name, localMachine))
268 | {
269 | case EnableState.Error:
270 | break;
271 | case EnableState.None:
272 | if (localMachine) // For lm, want to return here right away.
273 | return EnableState.None;
274 |
275 | // For current user, remember that we found no Enable value.
276 | if (state == EnableState.Error)
277 | {
278 | state = EnableState.None;
279 | }
280 | break;
281 | case EnableState.Disabled:
282 | state = EnableState.Disabled;
283 | break;
284 | case EnableState.Enabled:
285 | return EnableState.Enabled;
286 | }
287 | }
288 |
289 | return state;
290 | }
291 |
292 | #endregion Private Methods
293 |
294 | //------------------------------------------------------
295 | //
296 | // Private Properties
297 | //
298 | //------------------------------------------------------
299 |
300 | //------------------------------------------------------
301 | //
302 | // Private Fields
303 | //
304 | //------------------------------------------------------
305 |
306 | #region Private Fields
307 |
308 | // String consts used to validate registry entires.
309 | private const int CLSIDLength = 38; // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
310 | private const int LANGIDLength = 10; // 0x12345678
311 |
312 | // Status of a TIP assembly.
313 | private enum EnableState
314 | {
315 | Error, // Invalid entry.
316 | None, // No explicit Enable entry on the assembly.
317 | Enabled, // Assembly is enabled.
318 | Disabled // Assembly is disabled.
319 | };
320 |
321 | // Callback delegate for the IterateSubKeys method.
322 | private delegate EnableState IterateHandler(RegistryKey key, string subKeyName, bool localMachine);
323 |
324 | // Install state.
325 | private enum InstallState
326 | {
327 | Unknown, // Haven't checked to see if any TIPs are installed yet.
328 | Installed, // Checked and installed.
329 | NotInstalled // Checked and not installed.
330 | }
331 |
332 | // Cached install state value.
333 | // Writes are not thread safe, but we don't mind the neglible perf hit
334 | // of potentially writing it twice.
335 | private static InstallState s_servicesInstalled = InstallState.Unknown;
336 | private static object s_servicesInstalledLock = new object();
337 |
338 | #endregion Private Fields
339 | }
340 | }
341 |
--------------------------------------------------------------------------------
/ImeSharp/TextStore.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Buffers;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.Runtime.InteropServices;
6 | using ImeSharp.Native;
7 | using SharpGen.Runtime;
8 | using SharpGen.Runtime.Win32;
9 | using TsfSharp;
10 |
11 | namespace ImeSharp
12 | {
13 | internal class TextStore : CallbackBase,
14 | ITextStoreACP,
15 | ITfContextOwnerCompositionSink,
16 | ITfTextEditSink,
17 | ITfUIElementSink
18 | {
19 | public static readonly Guid IID_ITextStoreACPSink = new Guid(0x22d44c94, 0xa419, 0x4542, 0xa2, 0x72, 0xae, 0x26, 0x09, 0x3e, 0xce, 0xcf);
20 | public static readonly Guid GUID_PROP_COMPOSING = new Guid("e12ac060-af15-11d2-afc5-00105a2799b5");
21 |
22 | //------------------------------------------------------
23 | //
24 | // Constructors
25 | //
26 | //------------------------------------------------------
27 |
28 | #region Constructors
29 |
30 | // Creates a TextStore instance.
31 | public TextStore(IntPtr windowHandle)
32 | {
33 | _windowHandle = windowHandle;
34 |
35 |
36 | _viewCookie = Environment.TickCount;
37 |
38 | _editCookie = Tsf.TF_INVALID_COOKIE;
39 | _uiElementSinkCookie = Tsf.TF_INVALID_COOKIE;
40 | _textEditSinkCookie = Tsf.TF_INVALID_COOKIE;
41 | }
42 |
43 | #endregion Constructors
44 |
45 | //------------------------------------------------------
46 | //
47 | // Methods - ITextStoreACP
48 | //
49 | //------------------------------------------------------
50 |
51 | #region ITextStoreACP
52 |
53 | public void AdviseSink(Guid riid, IUnknown obj, int flags)
54 | {
55 | ITextStoreACPSink sink;
56 |
57 | if (riid != IID_ITextStoreACPSink)
58 | throw new COMException("TextStore_CONNECT_E_CANNOTCONNECT");
59 |
60 | sink = (obj as ComObject).QueryInterface();
61 |
62 | if (sink == null)
63 | throw new COMException("TextStore_E_NOINTERFACE");
64 |
65 | // It's legal to replace existing sink.
66 | if (_sink != null)
67 | _sink.Dispose();
68 |
69 | (obj as ComObject).Dispose();
70 |
71 | _sink = sink;
72 | }
73 |
74 | public void UnadviseSink(IUnknown obj)
75 | {
76 | var sink = (obj as ComObject).QueryInterface();
77 | if (sink.NativePointer != _sink.NativePointer)
78 | throw new COMException("TextStore_CONNECT_E_NOCONNECTION");
79 |
80 | _sink.Release();
81 | _sink = null;
82 | }
83 |
84 | private bool _LockDocument(TsfSharp.TsLfFlags dwLockFlags)
85 | {
86 | if (_locked)
87 | return false;
88 |
89 | _locked = true;
90 | _lockFlags = dwLockFlags;
91 |
92 | return true;
93 | }
94 |
95 | private void ResetIfRequired()
96 | {
97 | if (!_commited)
98 | return;
99 |
100 | _commited = false;
101 |
102 | TsTextchange textChange;
103 | textChange.AcpStart = 0;
104 | textChange.AcpOldEnd = _inputBuffer.Count;
105 | textChange.AcpNewEnd = 0;
106 | _inputBuffer.Clear();
107 |
108 | _sink.OnTextChange(0, textChange);
109 |
110 | _acpStart = _acpEnd = 0;
111 | _sink.OnSelectionChange();
112 | _commitStart = _commitLength = 0;
113 |
114 | //Debug.WriteLine("TextStore reset!!!");
115 | }
116 |
117 | private void _UnlockDocument()
118 | {
119 | Result hr;
120 | _locked = false;
121 | _lockFlags = 0;
122 |
123 | ResetIfRequired();
124 |
125 | //if there is a queued lock, grant it
126 | if (_lockRequestQueue.Count > 0)
127 | {
128 | hr = RequestLock(_lockRequestQueue.Dequeue());
129 | }
130 |
131 | //if any layout changes occurred during the lock, notify the manager
132 | if (_layoutChanged)
133 | {
134 | _layoutChanged = false;
135 | _sink.OnLayoutChange(TsLayoutCode.TsLcChange, _viewCookie);
136 | }
137 | }
138 |
139 | private bool _IsLocked(TsfSharp.TsLfFlags dwLockType)
140 | {
141 | return _locked && (_lockFlags & dwLockType) != 0;
142 | }
143 |
144 | public Result RequestLock(TsfSharp.TsLfFlags dwLockFlags)
145 | {
146 | Result hrSession;
147 |
148 | if (_sink == null)
149 | throw new COMException("TextStore_NoSink");
150 |
151 | if (dwLockFlags == 0)
152 | throw new COMException("TextStore_BadLockFlags");
153 |
154 | hrSession = Result.Fail;
155 |
156 | if (_locked)
157 | {
158 | //the document is locked
159 |
160 | if ((dwLockFlags & TsfSharp.TsLfFlags.Sync) == TsfSharp.TsLfFlags.Sync)
161 | {
162 | /*
163 | The caller wants an immediate lock, but this cannot be granted because
164 | the document is already locked.
165 | */
166 | hrSession = (int)TsErrors.TsESynchronous;
167 | }
168 | else
169 | {
170 | //the request is asynchronous
171 |
172 | //Queue the lock request
173 | _lockRequestQueue.Enqueue(dwLockFlags);
174 | hrSession = (int)TsErrors.TsSAsync;
175 | }
176 |
177 | return hrSession;
178 | }
179 |
180 | //lock the document
181 | _LockDocument(dwLockFlags);
182 |
183 | //call OnLockGranted
184 | hrSession = _sink.OnLockGranted(dwLockFlags);
185 |
186 | //unlock the document
187 | _UnlockDocument();
188 |
189 | return hrSession;
190 | }
191 |
192 | public TsStatus GetStatus()
193 | {
194 | TsStatus status = new TsStatus();
195 | status.DynamicFlags = 0;
196 | status.StaticFlags = 0;
197 |
198 | return status;
199 | }
200 |
201 | public void QueryInsert(int acpTestStart, int acpTestEnd, uint cch, out int acpResultStart, out int acpResultEnd)
202 | {
203 | acpResultStart = acpResultEnd = 0;
204 |
205 | // Fix possible crash
206 | if (_inputBuffer.Count == 0)
207 | return;
208 |
209 | //Queryins
210 | if (acpTestStart > _inputBuffer.Count || acpTestEnd > _inputBuffer.Count)
211 | throw new COMException("", Result.InvalidArg.Code);
212 |
213 | //Microsoft Pinyin seems does not init the result value, so we set the test value here, in case crash
214 | acpResultStart = acpTestStart;
215 | acpResultEnd = acpTestEnd;
216 | }
217 |
218 | public uint GetSelection(uint index, ref TsSelectionAcp selection)
219 | {
220 | //does the caller have a lock
221 | if (!_IsLocked(TsLfFlags.Read))
222 | {
223 | //the caller doesn't have a lock
224 | //return NativeMethods.TS_E_NOLOCK;
225 | throw new COMException("", (int)TsErrors.TsENolock);
226 | }
227 |
228 | //check the requested index
229 | if (-1 == (int)index)
230 | {
231 | index = 0;
232 | }
233 | else if (index > 1)
234 | {
235 | /*
236 | The index is too high. This app only supports one selection.
237 | */
238 | throw new COMException("", Result.InvalidArg.Code);
239 | }
240 |
241 | selection.AcpStart = _acpStart;
242 | selection.AcpEnd = _acpEnd;
243 | selection.Style.InterimCharFlag = _interimChar;
244 |
245 | if (_interimChar)
246 | {
247 | /*
248 | fInterimChar will be set when an intermediate character has been
249 | set. One example of when this will happen is when an IME is being
250 | used to enter characters and a character has been set, but the IME
251 | is still active.
252 | */
253 | selection.Style.Ase = TsActiveSelEnd.TsAeNone;
254 | }
255 | else
256 | {
257 | selection.Style.Ase = _activeSelectionEnd;
258 | }
259 |
260 | return 1;
261 | }
262 |
263 | public void SetSelection(uint count, ref TsSelectionAcp selections)
264 | {
265 | //this implementaiton only supports a single selection
266 | if (count != 1)
267 | throw new COMException("", Result.InvalidArg.Code);
268 |
269 | //does the caller have a lock
270 | if (!_IsLocked(TsLfFlags.Readwrite))
271 | {
272 | //the caller doesn't have a lock
273 | //return NativeMethods.TS_E_NOLOCK;
274 | throw new COMException("", (int)TsErrors.TsENolock);
275 | }
276 |
277 | _acpStart = selections.AcpStart;
278 | _acpEnd = selections.AcpEnd;
279 | _interimChar = selections.Style.InterimCharFlag;
280 |
281 | if (_interimChar)
282 | {
283 | /*
284 | fInterimChar will be set when an intermediate character has been
285 | set. One example of when this will happen is when an IME is being
286 | used to enter characters and a character has been set, but the IME
287 | is still active.
288 | */
289 | _activeSelectionEnd = TsActiveSelEnd.TsAeNone;
290 | }
291 | else
292 | {
293 | _activeSelectionEnd = selections.Style.Ase;
294 | }
295 |
296 | //if the selection end is at the start of the selection, reverse the parameters
297 | int lStart = _acpStart;
298 | int lEnd = _acpEnd;
299 |
300 | if (TsActiveSelEnd.TsAeStart == _activeSelectionEnd)
301 | {
302 | lStart = _acpEnd;
303 | lEnd = _acpStart;
304 | }
305 | }
306 |
307 |
308 | public void GetText(int acpStart, int acpEnd, System.IntPtr pchPlain, uint cchPlainReq, out uint cchPlainRet,
309 | ref TsfSharp.TsRuninfo rgRunInfo, uint cRunInfoReq, out uint cRunInfoRet, out int acpNext)
310 | {
311 | cchPlainRet = 0;
312 | cRunInfoRet = 0;
313 | acpNext = 0;
314 |
315 | //does the caller have a lock
316 | if (!_IsLocked(TsLfFlags.Read))
317 | {
318 | //the caller doesn't have a lock
319 | throw new COMException("", (int)TsErrors.TsENolock);
320 | }
321 |
322 | bool fDoText = cchPlainReq > 0;
323 | bool fDoRunInfo = cRunInfoReq > 0;
324 | int cchTotal;
325 |
326 | cchPlainRet = 0;
327 | acpNext = acpStart;
328 |
329 | cchTotal = _inputBuffer.Count;
330 |
331 | //validate the start pos
332 | if ((acpStart < 0) || (acpStart > cchTotal))
333 | {
334 | throw new COMException("", Result.InvalidArg.Code);
335 | }
336 | else
337 | {
338 | //are we at the end of the document
339 | if (acpStart == cchTotal)
340 | {
341 | return;
342 | }
343 | else
344 | {
345 | int cchReq;
346 |
347 | /*
348 | acpEnd will be -1 if all of the text up to the end is being requested.
349 | */
350 |
351 | if (acpEnd >= acpStart)
352 | {
353 | cchReq = acpEnd - acpStart;
354 | }
355 | else
356 | {
357 | cchReq = cchTotal - acpStart;
358 | }
359 |
360 | if (fDoText)
361 | {
362 | if (cchReq > cchPlainReq)
363 | {
364 | cchReq = (int)cchPlainReq;
365 | }
366 |
367 | //extract the specified text range
368 | if (pchPlain != IntPtr.Zero && cchPlainReq > 0)
369 | {
370 | //_inputBuffer.CopyTo(acpStart, pchPlain, 0, cchReq);
371 |
372 | unsafe
373 | {
374 | var ptr = (char*)pchPlain;
375 |
376 | for (int i = acpStart; i < cchReq; i++)
377 | {
378 | *ptr = _inputBuffer[i];
379 | ptr++;
380 | }
381 | }
382 | }
383 | }
384 |
385 | //it is possible that only the length of the text is being requested
386 | cchPlainRet = (uint)cchReq;
387 |
388 | if (fDoRunInfo)
389 | {
390 | /*
391 | Runs are used to separate text characters from formatting characters.
392 |
393 | In this example, sequences inside and including the <> are treated as
394 | control sequences and are not displayed.
395 |
396 | Plain text = "Text formatting."
397 | Actual text = "Text formatting."
398 |
399 | If all of this text were requested, the run sequence would look like this:
400 |
401 | prgRunInfo[0].type = TS_RT_PLAIN; //"Text "
402 | prgRunInfo[0].uCount = 5;
403 |
404 | prgRunInfo[1].type = TS_RT_HIDDEN; //
405 | prgRunInfo[1].uCount = 6;
406 |
407 | prgRunInfo[2].type = TS_RT_PLAIN; //"formatting"
408 | prgRunInfo[2].uCount = 10;
409 |
410 | prgRunInfo[3].type = TS_RT_HIDDEN; //
411 | prgRunInfo[3].uCount = 8;
412 |
413 | prgRunInfo[4].type = TS_RT_PLAIN; //"."
414 | prgRunInfo[4].uCount = 1;
415 |
416 | TS_RT_OPAQUE is used to indicate characters or character sequences
417 | that are in the document, but are used privately by the application
418 | and do not map to text. Runs of text tagged with TS_RT_OPAQUE should
419 | NOT be included in the pchPlain or cchPlainOut [out] parameters.
420 | */
421 |
422 | /*
423 | This implementation is plain text, so the text only consists of one run.
424 | If there were multiple runs, it would be an error to have consecuative runs
425 | of the same type.
426 | */
427 | rgRunInfo.Type = TsRunType.TsRtPlain;
428 | rgRunInfo.Count = (uint)cchReq;
429 | }
430 |
431 | acpNext = acpStart + cchReq;
432 | }
433 | }
434 | }
435 |
436 | public TsTextchange SetText(int dwFlags, int acpStart, int acpEnd, string pchText, uint cch)
437 | {
438 | /*
439 | dwFlags can be:
440 | TS_ST_CORRECTION
441 | */
442 | TsTextchange change = new TsTextchange();
443 |
444 | //set the selection to the specified range
445 | TsSelectionAcp tsa = new TsSelectionAcp();
446 | tsa.AcpStart = acpStart;
447 | tsa.AcpEnd = acpEnd;
448 | tsa.Style.Ase = TsActiveSelEnd.TsAeStart;
449 | tsa.Style.InterimCharFlag = false;
450 |
451 | SetSelection(1, ref tsa);
452 |
453 | int start, end;
454 | InsertTextAtSelection(TsIasFlags.Noquery, pchText, cch, out start, out end, out change);
455 |
456 | return change;
457 | }
458 |
459 | public IDataObject GetFormattedText(int startIndex, int endIndex)
460 | {
461 | throw new COMException("", Result.NotImplemented.Code);
462 | }
463 |
464 | public IUnknown GetEmbedded(int index, Guid guidService, Guid riid)
465 | {
466 | throw new COMException("", Result.NotImplemented.Code);
467 | }
468 |
469 | public RawBool QueryInsertEmbedded(Guid guidService, ref Formatetc formatEtc)
470 | {
471 | throw new COMException("", Result.NotImplemented.Code);
472 | }
473 |
474 | public TsTextchange InsertEmbedded(int flags, int startIndex, int endIndex, TsfSharp.IDataObject dataObjectRef)
475 | {
476 | throw new COMException("", Result.NotImplemented.Code);
477 | }
478 |
479 | public void InsertTextAtSelection(TsfSharp.TsIasFlags dwFlags, string pchText, uint cch, out int pacpStart, out int pacpEnd, out TsfSharp.TsTextchange pChange)
480 | {
481 | pacpStart = pacpEnd = 0;
482 | pChange = new TsTextchange();
483 |
484 | //does the caller have a lock
485 | if (!_IsLocked(TsLfFlags.Readwrite))
486 | {
487 | //the caller doesn't have a lock
488 | throw new COMException("", (int)TsErrors.TsENolock);
489 | }
490 |
491 | int acpStart;
492 | int acpOldEnd;
493 | int acpNewEnd;
494 |
495 | acpOldEnd = _acpEnd;
496 |
497 | //set the start point after the insertion
498 | acpStart = _acpStart;
499 |
500 | //set the end point after the insertion
501 | acpNewEnd = _acpStart + (int)cch;
502 |
503 | if ((dwFlags & TsIasFlags.Queryonly) == TsIasFlags.Queryonly)
504 | {
505 | pacpStart = acpStart;
506 | pacpEnd = acpOldEnd;
507 | return;
508 | }
509 |
510 | //insert the text
511 | _inputBuffer.RemoveRange(acpStart, acpOldEnd - acpStart);
512 | _inputBuffer.InsertRange(acpStart, pchText);
513 |
514 | //set the selection
515 | _acpStart = acpStart;
516 | _acpEnd = acpNewEnd;
517 |
518 | if ((dwFlags & TsIasFlags.Noquery) != TsIasFlags.Noquery)
519 | {
520 | pacpStart = acpStart;
521 | pacpEnd = acpNewEnd;
522 | }
523 |
524 | //set the TS_TEXTCHANGE members
525 | pChange.AcpStart = acpStart;
526 | pChange.AcpOldEnd = acpOldEnd;
527 | pChange.AcpNewEnd = acpNewEnd;
528 |
529 | //defer the layout change notification until the document is unlocked
530 | _layoutChanged = true;
531 | }
532 |
533 | public void InsertEmbeddedAtSelection(int flags, IDataObject obj, out int startIndex, out int endIndex, out TsTextchange change)
534 | {
535 | startIndex = endIndex = 0;
536 | change = new TsTextchange();
537 | throw new COMException("", Result.NotImplemented.Code);
538 | }
539 |
540 | public void RequestSupportedAttrs(int flags, uint cFilterAttrs, ref Guid filterAttributes)
541 | {
542 | }
543 |
544 | public void RequestAttrsAtPosition(int index, uint cFilterAttrs, ref Guid filterAttributes, int flags)
545 | {
546 | throw new COMException("", Result.NotImplemented.Code);
547 | }
548 |
549 |
550 | public void RequestAttrsTransitioningAtPosition(int position, uint cFilterAttrs, ref Guid filterAttributes, int flags)
551 | {
552 | throw new COMException("", Result.NotImplemented.Code);
553 | }
554 |
555 | public void FindNextAttrTransition(int startIndex, int haltIndex, uint cFilterAttrs, ref Guid filterAttributes, int flags, out int acpNext, out RawBool found, out int foundOffset)
556 | {
557 | acpNext = 0;
558 | found = false;
559 | foundOffset = 0;
560 | }
561 |
562 | public uint RetrieveRequestedAttrs(uint ulCount, ref TsfSharp.TsAttrval aAttrValsRef)
563 | {
564 | return 0;
565 | }
566 |
567 | public int GetEndACP()
568 | {
569 | int acp = 0;
570 | //does the caller have a lock
571 | if (!_IsLocked(TsLfFlags.Read))
572 | {
573 | //the caller doesn't have a lock
574 | throw new COMException("", (int)TsErrors.TsENolock);
575 | }
576 |
577 | acp = _inputBuffer.Count;
578 |
579 | return acp;
580 | }
581 |
582 | public int GetActiveView()
583 | {
584 | return _viewCookie;
585 | }
586 |
587 | public int GetACPFromPoint(int viewCookie, TsfSharp.Point tsfPoint, int dwFlags)
588 | {
589 | throw new COMException("", Result.NotImplemented.Code);
590 | }
591 |
592 | public void GetTextExt(int viewCookie, int acpStart, int acpEnd, out Rect rect, out RawBool clipped)
593 | {
594 | clipped = false;
595 | rect = InputMethod.TextInputRect;
596 |
597 | if (_viewCookie != viewCookie)
598 | throw new COMException("", Result.InvalidArg.Code);
599 |
600 | //does the caller have a lock
601 | if (!_IsLocked(TsLfFlags.Read))
602 | {
603 | //the caller doesn't have a lock
604 | throw new COMException("", (int)TsErrors.TsENolock);
605 | }
606 |
607 | //According to Microsoft's doc, an ime should not make empty request,
608 | //but some ime draw comp text themseleves, when empty req will be make
609 | //Check empty request
610 | //if (acpStart == acpEnd) {
611 | // return E_INVALIDARG;
612 | //}
613 |
614 | NativeMethods.MapWindowPoints(_windowHandle, IntPtr.Zero, ref rect, 2);
615 | }
616 |
617 | public Rect GetScreenExt(int viewCookie)
618 | {
619 | Rect rect = new Rect();
620 |
621 | if (_viewCookie != viewCookie)
622 | throw new COMException("", Result.InvalidArg.Code);
623 |
624 | NativeMethods.GetWindowRect(_windowHandle, out rect);
625 |
626 | return rect;
627 | }
628 |
629 | public IntPtr GetWnd(int viewCookie)
630 | {
631 | if (viewCookie != _viewCookie)
632 | {
633 | throw new COMException("", Result.False.Code);
634 | }
635 |
636 | return _windowHandle;
637 | }
638 |
639 | #endregion ITextStoreACP2
640 |
641 |
642 | //------------------------------------------------------
643 | //
644 | // Public Methods - ITfContextOwnerCompositionSink
645 | //
646 | //------------------------------------------------------
647 |
648 | #region ITfContextOwnerCompositionSink
649 |
650 | public RawBool OnStartComposition(ITfCompositionView view)
651 | {
652 | // Return true in ok to start the composition.
653 | RawBool ok = true;
654 | _compositionStart = _compositionLength = 0;
655 | _currentComposition.Clear();
656 |
657 | InputMethod.OnTextCompositionStarted(this);
658 | _compViews.Add(view);
659 |
660 | return ok;
661 | }
662 |
663 | public void OnUpdateComposition(ITfCompositionView view, ITfRange rangeNew)
664 | {
665 | var range = view.Range;
666 | var rangeacp = range.QueryInterface();
667 |
668 | rangeacp.GetExtent(out _compositionStart, out _compositionLength);
669 | rangeacp.Dispose();
670 | range.Dispose();
671 | _compViews.Add(view);
672 | }
673 |
674 | public void OnEndComposition(ITfCompositionView view)
675 | {
676 | var range = view.Range;
677 | var rangeacp = range.QueryInterface();
678 |
679 | rangeacp.GetExtent(out _commitStart, out _commitLength);
680 | rangeacp.Dispose();
681 | range.Dispose();
682 |
683 | // Ensure composition string reset
684 | _compositionStart = _compositionLength = 0;
685 | _currentComposition.Clear();
686 |
687 | InputMethod.ClearCandidates();
688 | InputMethod.OnTextCompositionEnded(this);
689 | view.Dispose();
690 | foreach(var item in _compViews)
691 | item.Dispose();
692 | _compViews.Clear();
693 | }
694 |
695 | #endregion ITfContextOwnerCompositionSink
696 |
697 | #region ITfTextEditSink
698 |
699 | public void OnEndEdit(ITfContext context, int ecReadOnly, ITfEditRecord editRecord)
700 | {
701 | ITfProperty property = context.GetProperty(GUID_PROP_COMPOSING);
702 |
703 | ITfRangeACP rangeACP = TextServicesContext.Current.ContextOwnerServices.CreateRange(_compositionStart, _compositionStart + _compositionLength);
704 | Variant val = property.GetValue(ecReadOnly, rangeACP);
705 | property.Dispose();
706 | rangeACP.Dispose();
707 | if (val.Value == null || (int)val.Value == 0)
708 | {
709 | if (_commitLength == 0 || _inputBuffer.Count == 0)
710 | return;
711 |
712 | //Debug.WriteLine("Composition result: {0}", new object[] { new string(_inputBuffer.GetRange(_commitStart, _commitLength).ToArray()) });
713 |
714 | _commited = true;
715 | for (int i = 0; i < _commitLength; i++)
716 | InputMethod.OnTextInput(this, _inputBuffer[_commitStart + i]);
717 | }
718 |
719 | if (_commited)
720 | return;
721 |
722 | if (_inputBuffer.Count == 0 && _compositionLength > 0) // Composition just ended
723 | return;
724 |
725 | _currentComposition.Clear();
726 | for (int i = 0; i < _compositionLength; i++)
727 | _currentComposition.Add(_inputBuffer[_compositionStart + i]);
728 |
729 | InputMethod.OnTextComposition(this, new IMEString(_currentComposition), _acpEnd);
730 |
731 | //var compStr = new string(_currentComposition.ToArray());
732 | //compStr = compStr.Insert(_acpEnd, "|");
733 | //Debug.WriteLine("Composition string: {0}, cursor pos: {1}", compStr, _acpEnd);
734 | }
735 |
736 | #endregion ITfTextEditSink
737 |
738 | //------------------------------------------------------
739 | //
740 | // Public Methods - ITfUIElementSink
741 | //
742 | //------------------------------------------------------
743 |
744 | #region ITfUIElementSink
745 |
746 | public RawBool BeginUIElement(int dwUIElementId)
747 | {
748 | // Hide OS rendered Candidate list Window
749 | RawBool pbShow = InputMethod.ShowOSImeWindow;
750 |
751 | OnUIElement(dwUIElementId, true);
752 |
753 | return pbShow;
754 | }
755 |
756 | public void UpdateUIElement(int dwUIElementId)
757 | {
758 | OnUIElement(dwUIElementId, false);
759 | }
760 |
761 | public void EndUIElement(int dwUIElementId)
762 | {
763 | }
764 |
765 | private void OnUIElement(int uiElementId, bool onStart)
766 | {
767 | if (InputMethod.ShowOSImeWindow || !_supportUIElement) return;
768 |
769 | ITfUIElement uiElement = TextServicesContext.Current.UIElementMgr.GetUIElement(uiElementId);
770 |
771 | ITfCandidateListUIElementBehavior candList;
772 |
773 | try
774 | {
775 | candList = uiElement.QueryInterface();
776 | }
777 | catch (SharpGenException)
778 | {
779 | _supportUIElement = false;
780 | return;
781 | }
782 | finally
783 | {
784 | uiElement.Dispose();
785 | }
786 |
787 | uint selection = 0;
788 | uint currentPage = 0;
789 | uint count = 0;
790 | uint pageCount = 0;
791 | uint pageStart = 0;
792 | uint pageSize = 0;
793 | uint i, j;
794 |
795 | selection = candList.GetSelection();
796 | currentPage = candList.GetCurrentPage();
797 |
798 | count = candList.GetCount();
799 |
800 | pageCount = candList.GetPageIndex(null, 0);
801 |
802 | if (pageCount > 0)
803 | {
804 | uint[] pageStartIndexes = ArrayPool.Shared.Rent((int)pageCount);
805 | pageCount = candList.GetPageIndex(pageStartIndexes, pageCount);
806 | pageStart = pageStartIndexes[currentPage];
807 |
808 | if (pageStart >= count - 1)
809 | {
810 | candList.Abort();
811 | ArrayPool.Shared.Return(pageStartIndexes);
812 | return;
813 | }
814 |
815 | if (currentPage < pageCount - 1)
816 | pageSize = Math.Min(count, pageStartIndexes[currentPage + 1]) - pageStart;
817 | else
818 | pageSize = count - pageStart;
819 |
820 | ArrayPool.Shared.Return(pageStartIndexes);
821 | }
822 |
823 | selection -= pageStart;
824 |
825 | IntPtr bStrPtr;
826 | for (i = pageStart, j = 0; i < count && j < pageSize; i++, j++)
827 | {
828 | bStrPtr = candList.GetString(i);
829 | InputMethod.CandidateList[j] = new IMEString(bStrPtr);
830 | }
831 |
832 | //Debug.WriteLine("TSF========TSF");
833 | //Debug.WriteLine("pageStart: {0}, pageSize: {1}, selection: {2}, currentPage: {3} candidates:", pageStart, pageSize, selection, currentPage);
834 | //for (int k = 0; k < candidates.Length; k++)
835 | // Debug.WriteLine(" {2}{0}.{1}", k + 1, candidates[k], k == selection ? "*" : "");
836 | //Debug.WriteLine("TSF++++++++TSF");
837 |
838 | InputMethod.CandidatePageSize = (int)pageSize;
839 | InputMethod.CandidateSelection = (int)selection;
840 |
841 | if (_currentComposition != null)
842 | {
843 | InputMethod.OnTextComposition(this, new IMEString(_currentComposition), _acpEnd);
844 | }
845 |
846 | candList.Dispose();
847 | }
848 |
849 | #endregion ITfUIElementSink
850 |
851 | //------------------------------------------------------
852 | //
853 | // Public Properties
854 | //
855 | //------------------------------------------------------
856 |
857 | public static TextStore Current
858 | {
859 | get
860 | {
861 | TextStore defaultTextStore = InputMethod.DefaultTextStore;
862 | if (defaultTextStore == null)
863 | {
864 | defaultTextStore = InputMethod.DefaultTextStore = new TextStore(InputMethod.WindowHandle);
865 |
866 | defaultTextStore.Register();
867 | }
868 |
869 | return defaultTextStore;
870 | }
871 | }
872 |
873 | public ITfDocumentMgr DocumentManager
874 | {
875 | get { return _documentMgr; }
876 | set { _documentMgr = value; }
877 | }
878 |
879 | // EditCookie for ITfContext.
880 | public int EditCookie
881 | {
882 | // get { return _editCookie; }
883 | set { _editCookie = value; }
884 | }
885 |
886 | public int UIElementSinkCookie
887 | {
888 | get { return _uiElementSinkCookie; }
889 | set { _uiElementSinkCookie = value; }
890 | }
891 |
892 | public int TextEditSinkCookie
893 | {
894 | get { return _textEditSinkCookie; }
895 | set { _textEditSinkCookie = value; }
896 | }
897 |
898 | public bool SupportUIElement { get { return _supportUIElement; } }
899 |
900 |
901 | //------------------------------------------------------
902 | //
903 | // Private Methods
904 | //
905 | //------------------------------------------------------
906 |
907 | // This function calls TextServicesContext to create TSF document and start transitory extension.
908 | private void Register()
909 | {
910 | // Create TSF document and advise the sink to it.
911 | TextServicesContext.Current.RegisterTextStore(this);
912 | }
913 |
914 | //------------------------------------------------------
915 | //
916 | // Private Fields
917 | //
918 | //------------------------------------------------------
919 |
920 | // The TSF document object. This is a native resource.
921 | private ITfDocumentMgr _documentMgr;
922 |
923 | private int _viewCookie;
924 |
925 | // The edit cookie TSF returns from CreateContext.
926 | private int _editCookie;
927 | private int _uiElementSinkCookie;
928 | private int _textEditSinkCookie;
929 |
930 | private ITextStoreACPSink _sink;
931 | private IntPtr _windowHandle;
932 | private int _acpStart;
933 | private int _acpEnd;
934 | private bool _interimChar;
935 | private TsActiveSelEnd _activeSelectionEnd;
936 | private List _inputBuffer = new List();
937 |
938 | private bool _locked;
939 | private TsLfFlags _lockFlags;
940 | private Queue _lockRequestQueue = new Queue();
941 | private bool _layoutChanged;
942 |
943 | private List _currentComposition = new List();
944 | private int _compositionStart;
945 | private int _compositionLength;
946 | private int _commitStart;
947 | private int _commitLength;
948 | private bool _commited;
949 |
950 | private bool _supportUIElement = true;
951 | private List _compViews = new List();
952 |
953 | }
954 | }
955 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 ryancheung
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # IME Sharp
2 | [](https://www.nuget.org/packages/ImeSharp/)
3 |
4 | A C# wrapper for Windows IME APIs. Its goal is to support both IMM32 and TSF.
5 |
6 | TSF Implementation is based on WPF core.
7 |
8 | ## Packages
9 |
10 | `dotnet add package ImeSharp`
11 |
12 | Note ImeSharp.NetStandard package is deprecated, use ImeSharp instead.
13 |
14 | ## Usage
15 |
16 | ### Initialization
17 |
18 | Call `InputMethod.Initialize` to initialize the input method with a window handle, e.g. `InputMethod.Initialize(someWindowHandle)`.
19 |
20 | If you don't want the OS Candidate Window, do `InputMethod.Initialize(someWindowHandle, false)`.
21 |
22 | ### Custom message pumping
23 |
24 | If we don't enable custom windows message pumping. Use TSF in WinForms would have a issue: Frame will randomly stuck when composing with IME.
25 | This is because TSF disables Application.Idle event when it's busy. Enables custom message pumping fix this.
26 |
27 | In WinForms, we add message pumping at the end line in `Application.Idle` handler, e.g.:
28 |
29 | ```c#
30 | private void Application_Idle(object sender, EventArgs e)
31 | {
32 | Game.Tick();
33 |
34 | // Enables custom message pumping
35 | InputMethod.PumpMessage();
36 | }
37 | ```
38 |
39 | ### Hook events
40 |
41 | ```c#
42 | InputMethod.TextInputCallback = OnTextInput;
43 | InputMethod.TextCompositionCallback = OnTextComposition;
44 | ```
45 |
46 | **Retrieve other composition info from `InputMethod.CandidateList` and other fields for CJK IMEs.**
47 |
48 | ### Set position of OS rendered IME Candidate Window
49 |
50 | ```c#
51 | InputMethod.SetTextInputRect(location.X, location.Y, 0, textBoxHeight);
52 | ```
53 |
54 | ## Test IMM32 implementation only
55 |
56 | IMM32 would be only enabled if TSF service is not available.
57 | You can `return false` manually in `TextServicesLoader.ServicesInstalled` to mimic TSF unavailable case.
58 |
59 | ## TODO
60 |
61 | - Make it work in Unity3d
62 |
63 | ## MS Docs
64 |
65 | - [TSF Application](https://docs.microsoft.com/en-us/windows/win32/tsf/applications)
66 | - [TSF UILess Mode](https://docs.microsoft.com/en-us/windows/win32/tsf/uiless-mode-overview)
67 | - [TSF msctf.h header](https://docs.microsoft.com/en-us/windows/win32/api/msctf/)
68 | - [IMM32 Use IME in a Game](https://docs.microsoft.com/en-us/windows/win32/dxtecharts/using-an-input-method-editor-in-a-game)
69 | - [IMM32 imm.h header](https://docs.microsoft.com/en-us/windows/win32/api/imm/)
70 |
71 | ## Other samples / implementations
72 |
73 | - [Chromium](https://github.com/chromium/chromium/tree/master/ui/base/ime/win)
74 | - [Windows Class Samples](https://github.com/microsoft/Windows-classic-samples/blob/master/Samples/IME/cpp/SampleIME)
75 | - [SDL2](https://github.com/spurious/SDL-mirror/blob/master/src/video/windows/SDL_windowskeyboard.c)
76 | - [WPF Core](https://github.com/dotnet/wpf/tree/master/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input)
77 |
78 | ## Credits
79 |
80 | - [WPF Core](https://github.com/dotnet/wpf)
81 | - [Chromium](https://github.com/chromium/chromium)
82 |
--------------------------------------------------------------------------------
/build.cake:
--------------------------------------------------------------------------------
1 | #tool nuget:?package=vswhere&version=3.1.7
2 |
3 | //////////////////////////////////////////////////////////////////////
4 | // ARGUMENTS
5 | //////////////////////////////////////////////////////////////////////
6 |
7 | var target = Argument("build-target", "Default");
8 | var version = Argument("build-version", EnvironmentVariable("BUILD_NUMBER") ?? "1.0.0");
9 | var configuration = Argument("build-configuration", "Release");
10 | var apiKey = Argument("api-key", "");
11 |
12 | //////////////////////////////////////////////////////////////////////
13 | // PREPARATION
14 | //////////////////////////////////////////////////////////////////////
15 |
16 | MSBuildSettings msPackSettings;
17 | DotNetMSBuildSettings dnBuildSettings;
18 | DotNetPackSettings dnPackSettings;
19 |
20 | private void PackDotnet(string filePath)
21 | {
22 | DotNetPack(filePath, dnPackSettings);
23 | }
24 |
25 | private void PackMSBuild(string filePath)
26 | {
27 | MSBuild(filePath, msPackSettings);
28 | }
29 |
30 | private bool GetMSBuildWith(string requires)
31 | {
32 | if (IsRunningOnWindows())
33 | {
34 | DirectoryPath vsLatest = VSWhereLatest(new VSWhereLatestSettings { Requires = requires });
35 |
36 | if (vsLatest != null)
37 | {
38 | var files = GetFiles(vsLatest.FullPath + "/**/MSBuild.exe");
39 | if (files.Any())
40 | {
41 | msPackSettings.ToolPath = files.First();
42 | return true;
43 | }
44 | }
45 | }
46 |
47 | return false;
48 | }
49 |
50 | var NuGetToolPath = Context.Tools.Resolve ("nuget.exe");
51 |
52 | var RunProcess = new Action ((process, args) =>
53 | {
54 | var result = StartProcess (process, args);
55 | if (result != 0) {
56 | throw new Exception ($"Process '{process}' failed with error: {result}");
57 | }
58 | });
59 |
60 | //////////////////////////////////////////////////////////////////////
61 | // TASKS
62 | //////////////////////////////////////////////////////////////////////
63 |
64 | Task("Prep")
65 | .Does(() =>
66 | {
67 | Console.WriteLine("Build Version: {0}", version);
68 |
69 | msPackSettings = new MSBuildSettings();
70 | msPackSettings.Verbosity = Verbosity.Minimal;
71 | msPackSettings.Configuration = configuration;
72 | msPackSettings.Restore = true;
73 | msPackSettings.WithProperty("Version", version);
74 | msPackSettings.WithTarget("Pack");
75 |
76 | dnBuildSettings = new DotNetMSBuildSettings();
77 | dnBuildSettings.WithProperty("Version", version);
78 |
79 | dnPackSettings = new DotNetPackSettings();
80 | dnPackSettings.MSBuildSettings = dnBuildSettings;
81 | dnPackSettings.Verbosity = DotNetVerbosity.Minimal;
82 | dnPackSettings.Configuration = configuration;
83 | });
84 |
85 | Task("Build")
86 | .IsDependentOn("Prep")
87 | .WithCriteria(() => IsRunningOnWindows())
88 | .Does(() =>
89 | {
90 | DotNetRestore("ImeSharp/ImeSharp.csproj");
91 | PackDotnet("ImeSharp/ImeSharp.csproj");
92 | });
93 |
94 | Task("Default")
95 | .IsDependentOn("Build");
96 |
97 | Task("Publish")
98 | .IsDependentOn("Default")
99 | .Does(() =>
100 | {
101 | var args = $"push -Source \"https://api.nuget.org/v3/index.json\" -ApiKey {apiKey} Artifacts/WinForms/Release/ImeSharp.{version}.nupkg";
102 |
103 | RunProcess(NuGetToolPath, args);
104 | });
105 |
106 | //////////////////////////////////////////////////////////////////////
107 | // EXECUTION
108 | //////////////////////////////////////////////////////////////////////
109 |
110 | RunTarget(target);
111 |
--------------------------------------------------------------------------------
/nuget.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------