エディタの行番号には論理行と物理行があります。両者はどう違うのかですが、これによると諸説あるそうです。ただ改行を1行と見なすのが論理行というのが多数派のようです。
前回のプログラム
これは表示行を表示するものでした。今回は行数は改行コードで数えます。
ではどうすればいいのでしょうか。行番号を表示するメソッドとして以下のようなものを作りました。
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 |
void DrawLineNumber() { int lineNum = 0; int height = richTextBox1.Size.Height; Graphics g = this.CreateGraphics(); g.Clear(Color.White); int charIndex = richTextBox1.GetCharIndexFromPosition(new Point(0, 0)); lineNum = richTextBox1.GetLineFromCharIndex(charIndex); while(true) { charIndex = richTextBox1.GetFirstCharIndexFromLine(lineNum); if(charIndex == -1) break; // charIndexは表示行の先頭であることは間違いないが、論理行の先頭なのか? Point pt = richTextBox1.GetPositionFromCharIndex(charIndex); Font f = new Font("MS 明朝", 10, GraphicsUnit.Pixel); g.DrawString((lineNum + 1).ToString(), f, Brushes.Blue, new PointF(0, pt.Y)); lineNum++; if(height < pt.Y) break; } g.Dispose(); } |
GetFirstCharIndexFromLineの引数は表示行の行番号です。論理行の先頭である必要条件は表示行の先頭であることです。そこで前回同様、表示行の先頭を求めてこれが論理行の先頭であるかどうかを調べます。
論理行の先頭なのかどうかはその前の文字を調べて改行コードであるかどうかで判断することができます。また論理行の先頭であることがわかった場合、それは何行目なのかを知る必要があります。これはそれより前に改行コードが何個あるか数えればわかります。
まずRichTextBoxの内容が変更されたら表示されている文字列を取得しましょう。
1 2 3 4 5 6 |
protected string _text = ""; private void RichTextBox1_TextChanged(object sender, EventArgs e) { _text = richTextBox1.Text; // それからどうする? } |
スクロールされたとき、編集されたときに以下の処理をおこないます。
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 41 42 43 44 45 46 47 48 |
void DrawLineNumber2() { char[] chars = _text.ToArray(); // 論理行の行頭の文字インデックスと行数を格納するためのリスト List<LineHeadY> vs = new List<LineHeadY>(); int lineNum = 0; int height = richTextBox1.Size.Height; int charIndex = richTextBox1.GetCharIndexFromPosition(new Point(0, 0)); lineNum = richTextBox1.GetLineFromCharIndex(charIndex); while(true) { charIndex = richTextBox1.GetFirstCharIndexFromLine(lineNum); if(charIndex == -1) break; Point pt = richTextBox1.GetPositionFromCharIndex(charIndex); lineNum++; if(height < pt.Y) break; if(charIndex == 0) vs.Add(new LineHeadY(0, 0)); else if(chars[charIndex - 1] == '\n') vs.Add(new LineHeadY(charIndex, pt.Y)); // charIndex文字目は論理行の行頭である // それ以外はcharIndexは表示行の行頭であって論理行の行頭ではない } // 取得できた座標に行番号を表示する Graphics g = this.CreateGraphics(); g.Clear(Color.White); foreach(var o in vs) { Font f = new Font("MS 明朝", 10, GraphicsUnit.Pixel); int lineIndex2 = 0; if(o.CharIndex == 0) lineIndex2 = 0; else lineIndex2 = chars.Take(o.CharIndex).Where(x => x == '\n').Count(); g.DrawString((lineIndex2 + 1).ToString(), f, Brushes.Blue, new PointF(0, o.Y)); } g.Dispose(); } |
1 2 3 4 5 6 7 8 9 10 11 |
// 論理行の行頭の文字インデックスと行数を管理するためのクラス public class LineHeadY { public LineHeadY(int charIndex, int y) { CharIndex = charIndex; Y = y; } public int CharIndex = 0; public int Y = 0; } |
あとはスクロールされたときと編集されたときに自作したDrawLineNumber2()メソッドを呼べばいいのかというとそうではありません。
RichTextBox1_VScrollはスクロールされたときに呼び出されるハンドラ、RichTextBox1_TextChangedは文書が変更されたときに呼び出されるのですが、長い文字列が入力されてスクロールがおきるときはRichTextBox1_VScrollが先で、RichTextBox1_TextChangedはそのあとに呼び出されます。そのため以下のコードでは例外が発生する場合があります。
1 2 3 4 5 6 7 8 9 10 11 |
protected string _text = ""; private void RichTextBox1_TextChanged(object sender, EventArgs e) { _text = richTextBox1.Text; DrawLineNumber2(); } private void RichTextBox1_VScroll(object sender, EventArgs e) { DrawLineNumber2(); } |
そこでタイマーをつかって少し時間をずらします。
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
public partial class UserControlRich : UserControl { public UserControlRich() { InitializeComponent(); richTextBox1.TextChanged += RichTextBox1_TextChanged; richTextBox1.VScroll += RichTextBox1_VScroll; } private void RichTextBox1_VScroll(object sender, EventArgs e) { Timer timer = new Timer(); timer.Interval = 10; timer.Tick += Timer_Tick; timer.Start(); void Timer_Tick(object sender1, EventArgs e1) { Timer t = (Timer)sender1; t.Stop(); t.Dispose(); DrawLineNumber2(); } } protected string _text = ""; private void RichTextBox1_TextChanged(object sender, EventArgs e) { _text = richTextBox1.Text; DrawLineNumber2(); } private void UserControlRich_Paint(object sender, PaintEventArgs e) { DrawLineNumber2(); } void DrawLineNumber2() { char[] chars = _text.ToArray(); List<LineHeadY> vs = new List<LineHeadY>(); // ここに論理行の行頭の文字インデックスを格納する int lineNum = 0; int height = richTextBox1.Size.Height; int charIndex = richTextBox1.GetCharIndexFromPosition(new Point(0, 0)); lineNum = richTextBox1.GetLineFromCharIndex(charIndex); while(true) { charIndex = richTextBox1.GetFirstCharIndexFromLine(lineNum); if(charIndex == -1) break; Point pt = richTextBox1.GetPositionFromCharIndex(charIndex); lineNum++; if(height < pt.Y) break; if(charIndex == 0) vs.Add(new LineHeadY(0, 0)); else if(chars[charIndex - 1] == '\n') // charIndex文字目は論理行の行頭である vs.Add(new LineHeadY(charIndex, pt.Y)); // それ以外はcharIndexは表示行の行頭であって論理行の行頭ではない } Graphics g = this.CreateGraphics(); g.Clear(Color.White); foreach(var o in vs) { Font f = new Font("MS 明朝", 10, GraphicsUnit.Pixel); int lineIndex2 = 0; if(o.CharIndex == 0) lineIndex2 = 0; else lineIndex2 = chars.Take(o.CharIndex).Where(x => x == '\n').Count(); g.DrawString((lineIndex2 + 1).ToString(), f, Brushes.Blue, new PointF(0, o.Y)); } g.Dispose(); } public class LineHeadY { public LineHeadY(int charIndex, int y) { CharIndex = charIndex; Y = y; } public int CharIndex = 0; public int Y = 0; } } |
これだと問題はおきません。
以下は上記のクラスを継承したクラスです。カーソルが移動するとカーソル位置がある論理行の行数と行頭からの位置を表示させることができます。
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 41 42 43 44 45 46 47 48 49 50 |
public class UserControlRichEx : UserControlRich { public UserControlRichEx() { this.richTextBox1.SelectionChanged += (sender, e) => OnSelectionChanged(e); } public delegate void SelectionChangedHandler(object sender, SelectionChangedArg e); public event SelectionChangedHandler SelectionChanged; private void OnSelectionChanged(EventArgs e) { Timer timer = new Timer(); timer.Interval = 10; timer.Tick += Timer_Tick; timer.Start(); void Timer_Tick(object sender1, EventArgs e1) { Timer t = (Timer)sender1; t.Stop(); t.Dispose(); ShowCursorPossition(); } } void ShowCursorPossition() { int start = richTextBox1.SelectionStart; Char[] chars = _text.ToArray(); int lineNum = chars.Take(start).Where(x => x == '\n').Count(); int a = _text.Substring(0, start).LastIndexOf("\n"); int index; if(a == -1) index = start; else index = start - a - 1; SelectionChangedArg arg = new SelectionChangedArg(); arg.lineNum = lineNum + 1; // 先頭が0ではなく1になるようにする arg.index = index + 1; // 同上 SelectionChanged?.Invoke(this, arg); } } public class SelectionChangedArg { public int lineNum = 0; public int index = 0; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public partial class Form1 : Form { public Form1() { InitializeComponent(); userControlRichEx1.SelectionChanged += UserControlRichEx1_SelectionChanged; } private void UserControlRichEx1_SelectionChanged(object sender, SelectionChangedArg e) { textBox1.Text = String.Format("{0}行{1}文字目", e.lineNum, e.index); } } |