Google Chart を使った数式ツールを作ってみた(3)
ネスト(入れ子)が認識できない。あと、[Shift]+[Tab]キーで逆向きに移動したいけれど、これがなかなかめんどくさい。{} だけじゃなくて () にも対応させたい、なんて考えだすと破たんするのが目に見えてるし。
というわけで、解決策は正規表現か、構文解析かって感じなんだけど。正規表現も大変だし、しかも限界が見えているので、ここは頑張って簡単な構文解析をするべきかと思っている。
Google Chart を使った数式ツールを作ってみた(2) - だるろぐ
構文解析というか、対応する { と } をペアにして、その出現位置をメモる方向で考えてみた。括弧の種類が増えていけば破たんするけれど、とりあえず最初は動けばいいや。アルゴリズムは、
- テキストを先頭から一文字ずつ取り出して、
- { だったら [i, ?] をリストに保存。(i は { の出現位置、? は } の位置を保存するプレースホルダ)
- } だったら最後の ? へ出現位置を保存。
- これを文末まで繰り返す。
みたいな感じ。大雑把に言えば、{ は前から詰めて、} は後ろから詰める、と。
たとえば、
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
\ | f | r | a | c | { | \ | f | r | a | c | { | } | { | } | } |
だったら、
{ の出現位置 | (対応する)} の出現位置 |
5 | 15 |
11 | 12 |
13 | 14 |
こういうリストを得るのがゴールになるかな。もしかしたら再帰でイケるのかな? と思ったけど、よくわかんなかったので素直に for を使って書くことにした。あと、? には text.Length - 1 を突っ込んでおいた。int? にして null をプレースホルダにしてもよかったのだけれど、ちょっとめんどくさいかな、と思って。
private Dictionary<int, int> BuidBracketIndo(string text) { var result = new Dictionary<int, int>(); var last_index = text.Length - 1; for (int i = 0; i <= last_index; i++) { switch (text[i]) { case '{': result.Add(i, last_index); break; case '}': try { var item = result.Last(_ => _.Value == last_index); result[item.Key] = i; } catch { } break; } } return result; }
これを TextChanged イベントで呼んで括弧の対応位置情報を毎回リフレッシュし、[Tab]キーの入力時に利用する。
// 「\frac{}{}」(分数の TeX 表現)を挿入する。 // 挿入後は[Tab]キーの押し下げを一度行い、 // 最初の {} の間にキャレットを移動させる private void Button_Click_4(object sender, RoutedEventArgs e) { // 途中でFormulaText.SelectionStart == 0 になってしまう(?)ので // キャレット位置を保存しておく var start = FormulaText.SelectionStart; FormulaText.Text = FormulaText.Text .Remove(start, FormulaText.SelectionLength) .Insert(start, @"\frac{}{}"); FormulaText.Focus(); FormulaText.SelectionStart = start; // [Tab]キーの押し下げをエミュレート var tab_key_down_event_args = new KeyEventArgs( Keyboard.PrimaryDevice, PresentationSource.FromVisual(FormulaText), (int)DateTime.Now.Ticks, Key.Tab ); tab_key_down_event_args.RoutedEvent = Keyboard.PreviewKeyDownEvent; FormulaText_PreviewKeyDown(sender, tab_key_down_event_args); } // 括弧の位置情報: TextChanged イベントでリフレッシュされる private Dictionary<int, int> brackets = null; // キーの押し下げイベントを処理 private void FormulaText_PreviewKeyDown(object sender, KeyEventArgs e) { switch (e.Key) { case Key.Tab: try { /* [Tab]キーで次のブラケット内へ進む */ if ((Keyboard.Modifiers & ModifierKeys.Shift) != ModifierKeys.Shift) { var b = brackets.First(_ => FormulaText.SelectionStart < _.Key); FormulaText.SelectionStart = b.Key + 1; FormulaText.SelectionLength = b.Value - b.Key - 1; } /* [Shift]+[Tab]キーで前のブラケット内へ戻る */ else { var b = brackets.Last(_ => _.Key < FormulaText.SelectionStart - 1); FormulaText.SelectionStart = b.Key + 1; FormulaText.SelectionLength = b.Value - b.Key - 1; } } catch { /* 不正な操作を行ったらビープ音を鳴らす */ System.Media.SystemSounds.Beep.Play(); } finally { e.Handled = true; // イベントを握りつぶす! } break; case Key.Escape: /* 選択状態を解除する */ FormulaText.SelectionLength = 0; break; } }
ついでに[Esc]キーで選択を解除できるようにしておいた。手元ではちゃんと動いている気がするけど、もう少し様子を見てから、アイコンなんかをつけて公開しようかと思う。
追記
ご指摘感謝! ブログの方はそのままにしておくので適当に読み直してください。List<KeyValuePair<int,int>> に書き換えればいいんですよね?