9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#】Num+キーでの入力項目移動(フォーカス遷移):IMEモードの判定

Last updated at Posted at 2025-08-09

7いいね以上 or 4ストックでWinFormを使った実装例(とGitリポジトリ)を追記します! → 面倒だったけど頑張って書いた。

9いいね以上 」 6ストックで出来る限りTSFについて分かった事書きます。)
10いいね以上で実測結果を入れます。(最終更新)

単に面倒なだけのタダ働きクソ記事

翻訳タイトル:C# Moving Focus Between Input Fields with the Num+ Key: IME Mode Detection
特定のキー(Addキー)入力におけるコントロールのフォーカス移動

※これは単なるSEO対策です。

前置き

気まぐれで見つけた質問に注視(まあ守備範囲だったんで)
尚、初回はグローバルフック使えとか回答してしまった。

特定のキーで入力項目間移動するプログラムを作成したが、繰り返し操作を行うとエラーでプログラムが落ちてしまう。
https://learn.microsoft.com/ja-jp/answers/questions/5478020/question-5478020

この質問では、imm32.dllを使用してImmGetVirtualKey
を利用してIMEを検知するというアプローチをしているが

繰り返し操作をする内に、入力項目間移動が行えず全角「+」が入力されてしまう項目が現れ、
さらに操作を繰り返していると以下のエラーが発生しプログラムが落ちてしまいました。

と報告している。
恐らくこれも別のアプローチで解決できると思われるが情報の安売りはしないので他を当たるように。 というか比較的簡単に判明したんだが。
5いいね以下でimm32.dllを使った項目は削除
 →案件に対応するために書かざる負えなくなった。

以下はなんかしょーもないなと思いながら実装。つか別の手段探そうとかないわけ。

尚、特定のキーとは+キーのことで、恐らく顧客の手癖か何かなのだろう。真っ先に思いつくのは矢印キー辺りだし不可解だが。

目次


参考&関連リンク

C#のWPFでフォーカスを移動する
WPFが対象
次のタブオーダーのコントロールをアクティブに(選択、フォーカスを移動)する
dobonなのでWinform向け。
カーソルキー(方向キー)を用いてフォームのコントロールのフォーカスを移動させる
実装が冗長すぎる。
【WPF】フォーカス遷移について知っておきたい3つのキーワード

GitHub(WPF/Winform)

.net 9.0 VisualStudio 2022

更新日から1ヵ月後までに"維持費"としてStarを要求します。 Starを付けない場合は要らないと判断してリポジトリを非公開化します。相変わらずがめつい

ProJect内にはそれぞれ WPF版、TSF実装、Winform版が入っています。

WPF版

実行結果

以下のCodeを忠実に実装すれば以下の結果を得られる。
※モバイル端末で表示されない場合はリロードしてくだちい。

bandicam 2025-08-09 19-55-36-194_Harua.mp4.gif

案件用のXAML定義(やたらと多いのでAIに作らせた)

....簡易的に数個で良かったと思うが。

<Window x:Class="PlusButtonToShiftIndexer.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:PlusButtonToShiftIndexer"
        mc:Ignorable="d"
        Loaded="Window_Loaded"
        KeyDown="Window_KeyDown"
        PreviewKeyDown="Window_PreviewKeyDown"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="1*"/>
                <ColumnDefinition Width="1*"/>
                <ColumnDefinition Width="1*"/>
            </Grid.ColumnDefinitions>

            <!-- 左列 -->
            <StackPanel Grid.Column="0">
                <TextBlock Text="初期値-タブ有無" Margin="0,0,0,5"/>
                <RadioButton Content="○あり" Margin="0,0,0,5"/>
                <RadioButton Content="×なし" Margin="0,0,0,5"/>
                <TextBlock Text="ラベル幅" Margin="0,0,0,5"/>
                <TextBox
                x:Name="inputer"
                PreviewKeyDown="TextBox_PreviewKeyDown" 
                  TextChanged="TextBox_TextChanged"
                  TextInput="inputer_TextInput"
                  PreviewTextInput="inputer_PreviewTextInput"
Text="文字" Width="100"/>
            </StackPanel>

            <!-- 中央列 -->
            <StackPanel Grid.Column="1">
                <TextBlock Text="制御コード設定" Margin="0,0,0,5"/>
                <CheckBox Content="○初期値" Margin="0,0,0,5"/>
                <CheckBox Content="入力項目幅" Margin="0,0,0,5"/>
                <StackPanel Orientation="Horizontal" Margin="0,0,0,5">
                    <TextBlock Text="活性" Margin="0,0,5,0"/>
                    <ComboBox Width="100">
                        <ComboBoxItem>あり</ComboBoxItem>
                        <ComboBoxItem>なし</ComboBoxItem>
                    </ComboBox>
                </StackPanel>
                <StackPanel Orientation="Horizontal" Margin="0,0,0,5">
                    <TextBlock Text="必須性" Margin="0,0,5,0"/>
                    <ComboBox Width="100">
                        <ComboBoxItem>あり</ComboBoxItem>
                        <ComboBoxItem>なし</ComboBoxItem>
                    </ComboBox>
                </StackPanel>
            </StackPanel>

            <!-- 右列 -->
            <StackPanel Grid.Column="2">
                <TextBlock Text="入力データ" Margin="0,0,0,5"/>
                <StackPanel Orientation="Horizontal" Margin="0,0,0,5">
                    <TextBlock Text="文字" Margin="0,0,5,0"/>
                    <TextBox Width="100"/>
                </StackPanel>
                <StackPanel Orientation="Horizontal" Margin="0,0,0,5">
                    <TextBlock Text="数値" Margin="0,0,5,0"/>
                    <TextBox Width="100"/>
                </StackPanel>
                <StackPanel Orientation="Horizontal" Margin="0,0,0,5">
                    <TextBlock Text="整数" Margin="0,0,5,0"/>
                    <TextBox Width="100"/>
                </StackPanel>
                <StackPanel Orientation="Horizontal" Margin="0,0,0,5">
                    <TextBlock Text="数値 (DECIMAL)" Margin="0,0,5,0"/>
                    <TextBox Width="100"/>
                </StackPanel>
                <StackPanel Orientation="Horizontal" Margin="0,0,0,5">
                    <TextBlock Text="日付データ" Margin="0,0,5,0"/>
                    <TextBox Width="100"/>
                </StackPanel>
            </StackPanel>
        </Grid>

        <!-- フッター部分 -->
        <StackPanel Grid.Row="1" VerticalAlignment="Bottom" Orientation="Horizontal" Margin="0,10,0,0">
            <Button Content="Enter" Width="100" Margin="0,0,10,0"/>
            <Button Content="F1: 取消" Width="100"/>
            <Button Content="F11: 確定" Width="100"/>
            <Button Content="Shift+F9: 終了" Width="100"/>
        </StackPanel>
    </Grid>
</Window>



スクショは案件に配慮して貼らないで置く。
実行画面貼るクセがあるんで意味なかった件(笑)

例のごとくe.Handledを使う

ふつーは真っ先に思いつくと思うが該当のイベントが見つからなかったのだろう。
TextBoxのイベントはPreviewTextInputイベントで入力直前のイベントをキャンセル出来ると分かった。
似たような物だと思うが最終的にPreviewKeyDownを採用。
Winformはコメントで要望あったらやる(但し5いいね以上)

参考:UIElement.PreviewTextInput イベント

イベント関係はいちいち把握しないといけないから面倒ですね。

なお、この案件ではcheckBoxでも同じくキャンセル実装が必要(だがめんどいから省く


  private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
  {
      //nugetから  InputSimulatorをインストール
      InputSimulator simulator = new InputSimulator();


      if (e.Key == Key.Add)
      {
         simulator.Keyboard.KeyPress(VirtualKeyCode.TAB); // Tabキーを送信
         e.Handled = true; // イベントを処理済みにする
      }
}
  

IMEの判定を行う(WPF・Winform兼用)

1ヵ月以内に5いいね以上付かない場合この項目は削除されます!

以下のように、IMEが有効時に+キーが入力されてしまう問題への対応。結局書く羽目になった。不運

bandicam 2025-08-10 01-11-24-382_Harua.mp4.gif

ime Modeが有効どうかを判定

注意点:ImmGetContextを使用する場合はリソースの解放が必要



     [DllImport("imm32.dll")]
        private static extern IntPtr ImmGetContext(IntPtr hWnd); 
        //指定されたウィンドウに関連付けられている入力コンテキストを取得します。
     [DllImport("imm32.dll")]
        private static extern bool ImmGetOpenStatus(IntPtr hIMC);  
        ///IME が開いているかどうかを調べます。
     [DllImport("imm32.dll")]
  private static extern uint ImmGetVirtualKey(IntPtr hWnd);
        //IMEが有効になる前の元々のキーコードを取得できる関数
     [DllImport("imm32.dll")]
   private static extern bool ImmReleaseContext(IntPtr hWnd, IntPtr hIMC);
        //ImmGetContextで使用したhWnd、hIMCを解放(解放しないと不安定になる)



     private const int NI_CLOSECANDIDATE = 0x0011;
     private const int CPS_CANCEL = 0x0004;
     private const int NI_COMPOSITIONSTR = 0x0015;

  

   private bool IsImeOn()
   {
   //WindowInteropHelper:このクラスのメンバーを使用すると、呼び出し元は
   //Win32 HWND と WPF Windowの親 HWND に内部アクセスできます。
       WindowInteropHelper helper = new WindowInteropHelper(this);
       IntPtr hWnd = helper.Handle;
       IntPtr hIMC = ImmGetContext(hWnd);
       if (hIMC != IntPtr.Zero)
       {
           bool isImeOpen = ImmGetOpenStatus(hIMC);
           return isImeOpen;
       }
       return false;
   }

参考

事前にフォーカスしておく

   private IntPtr winHandle; // ウィンドウハンドルをIntPtrで宣言

   private void Window_Loaded(object sender, RoutedEventArgs e)
   {
       var main = sender as Window;
       inputer.Focus();

       winHandle = new WindowInteropHelper(this).Handle;
   }

TextBoxのPreviewKeyDownイベント

private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e)
{

    IntPtr hWnd = winHandle;
    IntPtr hIMC = ImmGetContext(hWnd);

    if (hIMC != IntPtr.Zero)
    {
        uint virtualKey = ImmGetVirtualKey(hWnd);
        if (virtualKey != 0) // IMEが処理したキーがある場合
        {
            ImmNotifyIME(hIMC, NI_COMPOSITIONSTR, CPS_CANCEL, 0); // IMEバッファーをキャンセル

            if (IsImeOn()) //IMEがOnのとき
                e.Handled = true; // 入力イベントをキャンセル   
                
            //すぐにウィンドウハンドルを開放する
            ImmReleaseContext(hWnd, hIMC);
        }
    }
}

これでIMEがオンの時も+が入力されなくなることが証明された
bandicam 2025-08-10 03-33-48-396_Harua.mp4.gif

私はなんでどうでもいい他人の案件やってるんだろう

参考

IME関数一覧

キー送信

WPFで.net9.0だとSendKey使えないので、nugetからInputSimulatorをインストールする

ImmGetContext を使用したら必ずImmReleaseContextで解放すること

参考:https://katahiromz.web.fc2.com/colony3rd/imehackerz/ja/ImmReleaseContext.html

MainForm内のTextBoxを列挙する

今更触りたくないし。ただ間違いなく似たようなアプローチで解決する

こうすればコンストラクタでMainForm内の全てのTextBoxに対し自動的にPreviewKeyDownイベントを割り当てることが出来る。とても便利。

Codeは以下の記事を参考にしている。

WalkinChildren.cs

using System.Windows;

namespace PlusButtonToShiftIndexer
{
    static class DependencyObjectExtension
    {

        /// <summary>
        /// WalkInChildrenメソッドの本体
        /// </summary>
        /// <param name="obj">DependencyObject</param>
        /// <param name="act">Action</param>
        private static void Walk(DependencyObject obj, System.Action<DependencyObject> act)
        {
            foreach (var child in LogicalTreeHelper.GetChildren(obj))
            {
                if (child is DependencyObject)
                {
                    act(child as DependencyObject);
                    Walk(child as DependencyObject, act);
                }
            }
        }

        /// <summary>
        /// 子オブジェクトに対してデリゲートを実行する
        /// </summary>
        /// <param name="obj">this : DependencyObject</param>
        /// <param name="act">デリゲート : Action</param>
        public static void WalkInChildren(this DependencyObject obj, Action<DependencyObject> act)
        {
            if (act == null)
                throw new ArgumentNullException(obj.Dispatcher.ToString());

            Walk(obj, act);
        }
    }
}

コンストラクタ

 public MainWindow()
 {
     InitializeComponent();


     DependencyObjectExtension.WalkInChildren(this, (child) =>
     {
         if (child is TextBox textBox)
         {
             //全てのTextBoxにイベントを登録
             textBox.PreviewKeyDown += TextBox_PreviewKeyDown;


         }
     });

 }

TSF(Text Services Framework)を使ったフォーカス遷移 & IME判定

Move Focus Using the Text Services Framework (TSF)

※これ使った実装例が殆ど存在しないからいいね6以上を要求します。(投稿日から1か月後に未達だったらここだけ消します)

実行結果

実装例

TsfImeHelper.cs

IMEの判定は簡易的でTSF入力コンテキストが存在する=IMEが有効である」でIME Modeを間接的に検知している。

これで問題ないと思うがWin32APIの実装の方が確実。しかし、TSF実装の方がオーバーヘッドが少なくて軽いという利点がある。

※非static化し、IDsposableを継承させた。理由は下記参照。

TsfImeHelper.cs
using System.Runtime.InteropServices;

namespace PlusButtonToShiftIndexer.TSF
{
    public class TsfImeHelper : IDisposable
    {
        [ComImport, Guid("AA80E7F0-2021-11D2-93E0-0060B067B86E"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        private interface ITfThreadMgr
        {
            void Activate(out int clientId);
            void Deactivate();
            void CreateDocumentMgr(out ITfDocumentMgr docMgr);
            void EnumDocumentMgrs(out object enumDocMgrs);
            int GetFocus(out ITfDocumentMgr docMgr);
            void SetFocus(ITfDocumentMgr docMgr);
            void AssociateFocus(IntPtr hwnd, ITfDocumentMgr newDocMgr, out ITfDocumentMgr prevDocMgr);
            void IsThreadFocus([MarshalAs(UnmanagedType.Bool)] out bool isFocus);
        }

        private interface ITfDocumentMgr { /* 今回は空でOK */ }

        [DllImport("msctf.dll")]
        private static extern int TF_CreateThreadMgr(out ITfThreadMgr pptim);

        private ITfThreadMgr? _threadMgr;
        private int _tsfHResult;
        private bool _disposed;

        public TsfImeHelper()
        {
    
+            if (_threadMgr != null) return;
// 2重呼び出しをさけるため、コンストラクタから呼び出す
// 2重呼び出しだとハングする
+           _tsfHResult = TF_CreateThreadMgr(out _threadMgr);

            // HRESULT 成功判定
            if (_tsfHResult != 0 || _threadMgr == null)
            {
                // 初期化失敗時は解放
                _threadMgr = null;
            }
        }

        public bool IsImeActive()
        {
            if (_threadMgr == null || _tsfHResult != 0)
                return false;

            ITfDocumentMgr? docMgr = null;

            try
            {
                int hr = _threadMgr.GetFocus(out docMgr);
                if (hr != 0 || docMgr == null)
                    return false;

                // ここでIMEの詳細チェックを入れる場合は docMgr Contextを確認
                return true;
            }
            catch
            {
                return false;
            }
            finally
            {
                if (docMgr != null)
                    Marshal.ReleaseComObject(docMgr);
            }
        }

        public void Dispose()
        {
            if (_disposed) return;
            _disposed = true;

            if (_threadMgr != null && Marshal.IsComObject(_threadMgr))
            {
+                Marshal.ReleaseComObject(_threadMgr);
                _threadMgr = null;
            }
        }
    }
}

参考:Marshal.ReleaseComObject(Object) メソッド

外観

何の変哲もないUIだが、書かないのも不親切だろう。

<Window x:Class="PlusButtonToShiftIndexer.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:PlusButtonToShiftIndexer"
        mc:Ignorable="d"
        PreviewKeyDown="TextBox_PreviewKeyDown"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <TabControl>
            <TabItem Header="Test2">
                <Grid Margin="10">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="*"/>
                    </Grid.ColumnDefinitions>

                    <!-- TextBox -->
                    <TextBox x:Name="Test2_TextBox1" Grid.Row="0" Grid.Column="0" Margin="5" Text="TextBox 1"/>
                    <TextBox x:Name="Test2_TextBox2" Grid.Row="0" Grid.Column="1" Margin="5" Text="TextBox 2"/>
                    <TextBox x:Name="Test2_TextBox3" Grid.Row="1" Grid.Column="0" Margin="5" Text="TextBox 3"/>

                    <!-- Button -->
                    <Button x:Name="Test2_Button1" Grid.Row="1" Grid.Column="1" Margin="5" Content="Button 1"
                    Click="Test2_Button1_Click"
                    />
                    <Button x:Name="Test2_Button2" Grid.Row="2" Grid.Column="0" Margin="5" Content="Button 2"/>
                    <Button x:Name="Test2_Button3" Grid.Row="2" Grid.Column="1" Margin="5" Content="Button 3"/>

                    <!-- CheckBox -->
                    <CheckBox x:Name="Test2_CheckBox1" Grid.Row="3" Grid.Column="0" Margin="5" Content="CheckBox 1"/>
                    <CheckBox x:Name="Test2_CheckBox2" Grid.Row="3" Grid.Column="1" Margin="5" Content="CheckBox 2"/>
                    <CheckBox x:Name="Test2_CheckBox3" Grid.Row="4" Grid.Column="0" Margin="5" Content="CheckBox 3"/>
                </Grid>

            </TabItem>
        </TabControl>
    </Grid>
</Window>

MainWindowの実装

IMEバッファーのクリアは割愛したため、大分シンプル。


using PlusButtonToShiftIndexer.TSF;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace PlusButtonToShiftIndexer
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {

        public MainWindow()
        {
            InitializeComponent();
            DependencyObjectExtension.WalkInChildren(this, (child) =>
                        {
                            if (child is TextBox textBox)
                            {
                                textBox.PreviewKeyDown += TextBox_PreviewKeyDown;

                                if (child is UIElement uiElement)
                                {
                                    TextCompositionManager.AddPreviewTextInputHandler(uiElement, OnTextInput);
                                }
                            }
                        });
        }

        private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            var focused = Keyboard.FocusedElement;


           
            //IsImeActive() 自体でも threadMgr を作っている            
            using (var _tsf = new TsfImeHelper())
            { 
            // IMEオン時は無効
             if (e.Key == Key.Add && !_tsf.IsImeActive())
            {
                e.Handled = true; // 通常の入力をキャンセル


                var request = new TraversalRequest(FocusNavigationDirection.Next);
                var element = (FrameworkElement)sender;
                element.MoveFocus(request);

                // UIElementの場合
                if (focused is UIElement uie)
                {
                    bool moved = uie.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));


                    if (!moved)
                    {
                        request = new TraversalRequest(FocusNavigationDirection.First);

                        uie.MoveFocus(request);

                    }
                }

            }
        }
    }
}

IME BufferのクリアはWin32APIを使った方法以外には存在しない。
→ IMEBufferは読み取り専用(ReadOnly)。
変換候補を閉じることで間接的に実現できる

したがって前回と同じなので割愛する。

TF_CreateThreadMgr関数 (msctf.h)について

https://learn.microsoft.com/ja-jp/windows/win32/api/msctf/nf-msctf-tf_createthreadmgr

TF_CreateThreadMgr関数は、COM を初期化せずにスレッド マネージャー オブジェクトを作成します。 呼び出し元プロセスは、Msctf.dll が所有するオブジェクトに対して適切な参照カウントを維持する必要があるため、このメソッドの使用はお勧めしません。

とあるので、適切に破棄する必要がある。

どういうときに破棄すべきか

長時間動くアプリ(エディタ、チャットアプリなど)では、解放しないとプロセス内でメモリやハンドルを消費し続ける。
最悪の場合ハングするので、例外処理やIDisposableが必要である
尚、その前にアプリが終了されればリソースは適切に開放される。

ITfThreadMgr インターフェース(COM Interface)のGPUIDについて

生成AIが出した属性とGPUIDなんでリファレンスが必要だったんだけど、いくらググっても出てこないネタ元が謎だった。

//この部分
  [ComImport, Guid("AA80E7F0-2021-11D2-93E0-0060B067B86E"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  private interface ITfThreadMgr
  {

どうにか見つけ出したのはコレ
View the source code for any class in the .NET framework. (dotnetのソースコードを探せる)


/ DotNET / DotNET / 8.0 / untmp / WIN_WINDOWS / lh_tools_devdiv_wpf / Windows / wcp / Shared / MS / Win32 / unsafeNativemethodsTextservices.cs / 1 / unsafeNativemethodsTextservices.cs

から発見した
image.png

今後のソース調査の参考にしたい。(なんか以前も誰かが提示してたけど記事消したんで)

なんとも面倒なことにInterFace名ごとに一意のGPUIDが指定されているから、いちいち調べないといけないというわけ

どうりでWeb上に実装例が殆どないはずである。
使わせる気がないのかね....

Winform版の特定のキーによるフォーカス移動とIME判定(2025/8/16)

例によってTextBoxを対象にした際に入力されないようにしている。

※更新日より1ヵ月後までに 8いいね以上 or 5ストック以上なければこのカテゴリは消します。(限定公開にする)

Win32APIを使った実装はWPF版と同じ。
(強調表示が追加箇所)

注:TSFはWPF専用なので使えない。
  TextChangedではイベントのキャンセルが出来ない。

using System.Diagnostics;
using System.Runtime.InteropServices;

namespace NumPlusFocusNavigator_Winform
{
    public partial class Mainform : Form
    {
        [DllImport("imm32.dll")]
        private static extern IntPtr ImmGetContext(IntPtr hWnd);

        [DllImport("imm32.dll")]
        private static extern bool ImmGetOpenStatus(IntPtr hIMC);

        [DllImport("imm32.dll")]
        private static extern bool ImmReleaseContext(IntPtr hWnd, IntPtr hIMC);

        [DllImport("imm32.dll")]
        private static extern uint ImmGetVirtualKey(IntPtr hWnd);
        //IMEが有効になる前の元々のキーコードを取得できる関数


        [DllImport("imm32.dll")]
        private static extern bool ImmNotifyIME(IntPtr hIMC, int dwAction, int dwIndex, int dwValue);

        private const int NI_COMPOSITIONSTR = 0x0015;
        private const int CPS_CANCEL = 0x0004;

        public Mainform()
        {
            InitializeComponent();

            this.WalkInChildren(ctrl =>
            {
                if (ctrl is MyTextBox tbox)
                {
                    tbox.KeyPress += SendTabKey_KeyPress;

                    tbox.KeyDown += tbox_KeyDown;

                    tbox.KeyPress += textBox_KeyPress;
                }

            });

        }

        private void tbox_KeyDown(object? sender, KeyEventArgs e)
        {
            if (IsImeOn(Handle)) //IMEOnのとき
                e.Handled = true; // 入力イベントをキャンセル   


            IntPtr hWnd = this.Handle;
            IntPtr hIMC = ImmGetContext(hWnd);

+            if (hIMC != IntPtr.Zero)
            {
                uint virtualKey = ImmGetVirtualKey(hWnd);
                if (virtualKey != 0) // IMEが処理したキーがある場合
                {
                    ImmNotifyIME(hIMC, NI_COMPOSITIONSTR, CPS_CANCEL, 0); // IMEバッファーをキャンセル


                    //すぐにウィンドウハンドルを開放する
                    ImmReleaseContext(hWnd, hIMC);
                }
            }
        }

+        private void SendTabKey_KeyPress(object? sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == '+')
            {
                SendKeys.Send("{TAB}");
                e.Handled = true; // 入力を無効化したい場合
            }
        }





        private void Mainform_Load(object sender, EventArgs e)
        {
            myTextBox1.Focus(); 
        }

        private bool IsImeOn(IntPtr hWnd)
        {
            IntPtr hIMC = ImmGetContext(hWnd);
            if (hIMC != IntPtr.Zero)
            {
                bool isImeOpen = ImmGetOpenStatus(hIMC);
                ImmReleaseContext(hWnd, hIMC);
                return isImeOpen;
            }
            return false;
        }

  private void textBox_KeyPress(object? sender, KeyPressEventArgs? e)
  {
      if (e.KeyChar == '+')
        e.Handled = true; 
      else if (IsImeOn(Handle))
          e.Handled = true;
  }
     
}

Form内のTextBoxを列挙(取得)

EnumerateForm.cs
namespace NumPlusFocusNavigator_Winform
{
    internal static class EnumerateForm
    {
        /// <summary>
        /// 内部的に再帰で子コントロールを探索するメソッド
        /// </summary>
        private static void Walk(Control parent, Action<Control> act)
        {
            foreach (Control child in parent.Controls)
            {
                if (child == null) continue;
                if (act == null) return;

                act(child);
                Walk(child, act);  // 再帰
            }
        }

        /// <summary>
        /// 子コントロールに対してデリゲートを実行する拡張メソッド
        /// </summary>
        public static void WalkInChildren(this Control parent, Action<Control> act)
        {
            if (parent == null) throw new ArgumentNullException(nameof(parent));
            if (act == null) throw new ArgumentNullException(nameof(act));

            Walk(parent, act);
        }
    }
}

実行結果

bandicam 2025-08-17 01-48-27-130_Harua.mp4.gif

WndProcをOverRideするやり方も試したんだけど、TextBoxのIME入力と衝突したらしくハングして異常終了。沼った。
初心に却って単純な実装にした。

未確定状態の文字列を取得する方法(オーバーライド)

    protected override void WndProc(ref Message m)
    {
        if (m.Msg == WM_IME_COMPOSITION)
        {
            IntPtr hIMC = ImmGetContext(this.Handle);
            if (hIMC != IntPtr.Zero)
            {
                // 未確定文字列の長さを取得
                int size = ImmGetCompositionStringW(hIMC, GCS_COMPSTR, null, 0);
                if (size > 0)
                {
                    byte[] buffer = new byte[size];
                    ImmGetCompositionStringW(hIMC, GCS_COMPSTR, buffer, size);

                    string comp = Encoding.Unicode.GetString(buffer);
                    Console.WriteLine("未確定文字列: " + comp);
                }
                ImmReleaseContext(this.Handle, hIMC);
            }
        }
        base.WndProc(ref m);
    }

あとがき

まあ手札が増えた気もするんで良かったです(自分の完璧主義を呪っています)
それにしても十分対応できる案件だと思うが。
まず単純な実装を試そう。
IMEがONのとき+が入力されないようにするのは案外面倒でした。調べはついたけど

今回は省いたがIMEモードの判定も出来ると分かったのが収穫だった。

記事の作成と検証にも手間がかかるんだから仕事回すか、いいねするかお金ください。。。 ....そのうちQiitaの支援機能使おう。

....質問に答えるだけで大分長い記事になってしまいました。
とにかくいいねください、いいねしないなら遠慮なく消します。 →流石にもう消さないとおもう(かなり多分)

9
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?