1ヵ月以内に5いいねが付かない場合は消します!
3ストック以上でTSFを使った実装例を追記します!
5ストック以上でWinFormを使った実装例(とGitリポジトリ)を追記します!
翻訳タイトル: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つのキーワード
実行結果
以下のCodeを忠実に実装すれば以下の結果を得られる。
※モバイル端末で表示されない場合はリロードしてくだちい。
案件用の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の判定を行う
1ヵ月以内に5いいね以上付かない場合この項目は削除されます!
以下のように、IMEが有効時に+キー
が入力されてしまう問題への対応。結局書く羽目になった。不運
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がオンの時も+が入力されなくなることが証明された
私はなんでどうでもいい他人の案件やってるんだろう
参考
キー送信
WPFで.net9.0だとSendKey使えないので、nugetからInputSimulatorをインストールする
ImmGetContext を使用したら必ずImmReleaseContext
で解放すること
参考:https://katahiromz.web.fc2.com/colony3rd/imehackerz/ja/ImmReleaseContext.html
MainForm内のTextBoxを列挙する
Winform版は悪いけど自分で何とかしてくれ(笑)
今更触りたくないし。ただ間違いなく似たようなアプローチで解決する
こうすればコンストラクタでMainForm内の全てのTextBoxに対し自動的にPreviewKeyDown
イベントを割り当てることが出来る。とても便利。
Codeは以下の記事を参考にしている。
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;
}
});
}
あとがき
まあ手札が増えた気もするんで良かったです(自分の完璧主義を呪っています)
それにしても十分対応できる案件だと思うが。
まず単純な実装を試そう。
→ IMEがONのとき+が入力されないようにする
のは案外面倒でした。調べはついたけど
今回は省いたがIMEモードの判定も出来ると分かったのが収穫だった。
記事の作成と検証にも手間がかかるんだから仕事回すか、いいねするかお金ください。。。 ....そのうちQiitaの支援機能使おう。
こんなしょーもない記事を書きたいわけではない。
とにかくいいねください、いいねしないなら遠慮なく消します。