├── .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 | [![Nuget](https://img.shields.io/nuget/v/ImeSharp)](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 | --------------------------------------------------------------------------------