だるろぐ

明日できることは、今日しない。

Google Chart を使った数式ツールを作ってみた(3)

f:id:daruyanagi:20130115205946p:plain

ネスト(入れ子)が認識できない。あと、[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>> に書き換えればいいんですよね?