
まずはネーミング。"ExpandedTextBox"とでもしておきます。ちょっと長ったらしいかもしれません。ネーミングにはいつも悩みます。
Public Class ExpandedTextControl
Inherits System.Windows.Forms.RichTextBox
...
...
End Class
下記が変数の宣言です。m_pathnameは読み込んでいるファイル名を格納する内部変数です。PathNameプロパティに反映されます。m_tabwidthはタブ幅の値です。これもプロパティはTabWidthで別に宣言して外部からアクセスできるようにしています。セオリー通り、内部変数とプロパティを分けて設定しました。
後はポップアップメニューです。動的に配置するようにしています。デザイン画面で設計した方が簡単なのでしょうが、もりじょうにはこの方が楽だったりします。これらは、すべてWithEvensで宣言していません。別にContextMenuClickerというEventHandlerを宣言してClickイベントを結び付けています。こうすると一つの関数内ですべてのメニューのイベントを楽に処理することができます。ここでは"ContextMenu_Click"がそれに当たります。このページの後ろの方に出てきます。
最後の二行はイベント宣言です。Drag&Dropイベントを独自に設定したいのですが、これらはProtectedイベントにしかなく外部に公開できないためです。
' 変数
Private m_pathname As String = ""
Private m_tabwidth As Integer = 0
Private ContextMenuClicker As _
New EventHandler(AddressOf ContextMenu_Click)
Private WithEvents m_popup As New ContextMenu()
Private m_undo As New MenuItem("元に戻す(&U)", ContextMenuClicker)
Private m_cut As New MenuItem("切り取り(&T)", ContextMenuClicker)
Private m_copy As New MenuItem("コピー(&C)", ContextMenuClicker)
Private m_paste As New MenuItem("貼り付け(&P)", ContextMenuClicker)
Private m_delete As New MenuItem("削除(&D)", ContextMenuClicker)
Private m_select As New MenuItem("すべて選択(&A)", ContextMenuClicker)
Private m_split1 As New MenuItem("-")
Private m_split2 As New MenuItem("-")
' プロパティ : PathName
Public Property PathName() As String
Get
Return m_pathname
End Get
Set(ByVal Value As String)
m_pathname = Value
End Set
End Property
' プロパティ : TabWidth
Public ReadOnly Property TabWidth() As Integer
Get
Return m_tabwidth
End Get
End Property
' イベント
Public Event OnFileDrop(ByVal e As DragEventArgs)
Win32APIを使用する旨の宣言をする必要があります。メッセージ定数の定義もあわせて必要です。SendMessage関数は複数宣言してあります。引数の部分のデータ型や代入方法(ByVal/ByRef)が微妙に違ったりしています。IntPtr型はその変数のアドレスの位置を表す特殊な型です。ポインタはVBではほとんど使いませんが、Cでは必須項目です。ShellExecute...、で始まる関数と構造体はウインドウズの"ファイルのプロパティ"ダイアログを開くのに使います。
' 定数(Win32API)
Private Const IMF_DUALFONT As Integer = &H80
Private Const WM_USER As Integer = &H400
Private Const EM_SETLANGOPTIONS As Integer = WM_USER + 120
Private Const EM_GETLANGOPTIONS As Integer = WM_USER + 121
Private Const EM_FORMATRANGE As Integer = WM_USER + 57
Private Const EM_GETLINECOUNT As Integer = &HBA
Private Const EM_LINELENGTH As Integer = &HC1
Private Const EM_LINEINDEX As Integer = &HBB
Private Const EM_LINEFROMCHAR As Integer = &HC9
Private Const EM_SETTABSTOPS As Integer = &HCB
Private Const SEE_MASK_INVOKEIDLIST As Integer = &HC
' Win32API 関数
Private Declare Function SendMessage Lib "user32.dll" _
Alias "SendMessageA" _
(ByVal hWnd As IntPtr, _
ByVal Msg As Integer, _
ByVal wParam As Integer, _
ByVal lParam As Integer) As Integer
Private Declare Function SendMessage Lib "user32.dll" _
Alias "SendMessageA" _
(ByVal hWnd As IntPtr, _
ByVal Msg As Integer, _
ByVal wParam As Integer, _
ByRef lParam As Short) As Boolean
Private Declare Function SendMessage Lib "user32.dll" _
Alias "SendMessageA" _
(ByVal hWnd As IntPtr, _
ByVal msg As Integer, _
ByVal wParam As IntPtr, _
ByVal lParam As IntPtr) As IntPtr
Private Declare Function ShellExecuteEx Lib "shell32.dll" _
Alias "ShellExecuteExA" _
(ByRef lpExecInfo As ShellExecuteInfo) As Integer
' 構造体 : ShellExecuteInfo(Win32API)
Private Structure ShellExecuteInfo
Dim cbSize As Integer
Dim fMask As Integer
Dim hWnd As IntPtr
Dim lpVerb As String
Dim lpFile As String
Dim lpParameters As String
Dim lpDirectory As String
Dim nShow As Integer
Dim hInstApp As IntPtr
Dim lpIDList As Integer
Dim lpClass As String
Dim hkeyClass As IntPtr
Dim dwHotKey As Integer
Dim hIcon As IntPtr
Dim hProcess As IntPtr
End Structure
デザイナを使ってませんので、プロパティの初期化をコーディングしています。言語オプションの設定のところですが、まず言語オプション(LangOption)の現在の状態を取得、"DualFont"を解除した値を取得、言語オプションを更新、という手順になっています。"Xor"のくだりはビット演算ですね。私はこれに慣れるのに大変でした。
' コンストラクタ
Public Sub New()
MyBase.New()
' プロパティの設定
Me.BorderStyle = BorderStyle.None
Me.AcceptsTab = True
Me.AllowDrop = True
Me.HideSelection = False
' 言語オプションの設定
Dim nLangOptions As Integer = 0
SendMessage(Me.Handle, EM_GETLANGOPTIONS, 0, nLangOptions)
nLangOptions = nLangOptions Xor IMF_DUALFONT
SendMessage(Me.Handle, EM_SETLANGOPTIONS, 0, nLangOptions)
' コンテキストメニューの追加
m_popup.MenuItems.AddRange( New MenuItem() _
{ m_undo, m_split1, m_cut, m_copy, _
m_paste, m_delete, m_split2, m_select} )
Me.ContextMenu = m_popup
End Sub
OnDragEnterイベントではファイルのみ受け付けるようにしています。OnDragDropイベントで新たに定義したOnFileDropイベントを発生させています。
' ドラッグの受け入れ
Protected Overrides Sub OnDragEnter( _
ByVal drgevent As System.Windows.Forms.DragEventArgs)
If drgevent.Data.GetDataPresent(DataFormats.FileDrop) Then
drgevent.Effect = DragDropEffects.All
End If
MyBase.OnDragEnter(drgevent)
End Sub
' ドラッグドロップ
Protected Overrides Sub OnDragDrop( _
ByVal drgevent As System.Windows.Forms.DragEventArgs)
MyBase.OnDragDrop(drgevent)
RaiseEvent OnFileDrop(drgevent)
End Sub
Win32APIを経由してデータを取得するための関数群です。既存のメソッドに同じ機能を持つものがあるような気もします。
' GetLineCount : 行数の取得
Public Function GetLineCount() As Integer
Return SendMessage(Me.Handle, EM_GETLINECOUNT, 0, 0)
End Function
' GetLineLength : 指定された文字がある行の長さの取得
Public Function GetLineLength(ByVal CharIndex As Integer) As Integer
Return SendMessage(Me.Handle, EM_LINELENGTH, CharIndex, 0)
End Function
' GetCharIndex : 指定された行の先頭文字のインデックスを取得
Public Function GetCharIndex(ByVal LineIndex As Integer) As Integer
Return SendMessage(Me.Handle, EM_LINEINDEX, LineIndex, 0)
End Function
' GetLineIndex : 指定された文字の位置から行インデックスを取得
Public Function GetLineIndex(ByVal CharIndex As Integer) As Integer
Return SendMessage(Me.Handle, EM_LINEFROMCHAR, CharIndex, 0)
End Function
"ワードラップ"メニュー、"タブ幅"メニュー、"ジャンプ"メニュー、"プロパティ"メニューに対応する機能をコーディングします。SetWordWrap関数の引数IsWordWrapはメインフォーム側のコーディングで設定されます。わざわざModifiedプロパティの現状態を記憶して戻しています。WordWrapプロパティを操作するとModifiedプロパティがTrueになってしまうからです。リッチテキスト独自の機能を排除するための処理です。
タブストップのところで、タブ幅の指定は別途ダイアログで行ないます。ダイアログで指定した値が"PixelValue"に渡されることになります。デフォルトは8。実はこの単位はメモ帳などとは異なり、やや狭いと思います。ダイアログ単位なるものが絡んでいるようで、さらにこの単位はダイアログで使用するフォントサイズに影響されるようです。私の理解不足かもしれませんが、ダイアログ単位を取得する方法が面倒くさそうだったのでデフォルト値のままにしてしまっています。
' SetWordWrap : ワードラップの設定
Public Sub SetWordWrap(ByVal IsWordwrap As Boolean)
Dim bModified As Boolean = Me.Modified
Me.WordWrap = IsWordwrap
Me.Modified = bModified
End Sub
' SetTabStops : タブストップの設定
Public Sub SetTabStops(ByVal PixelValue As Integer)
If PixelValue <> m_tabwidth Then
' 選択文字列の位置を記憶
Dim nStart As Integer = Me.SelectionStart
Dim nLength As Integer = Me.SelectionLength
' タブ幅を算出して適用
Dim nTabwidth As Short = CShort(PixelValue * 4)
Dim bReturn As Boolean
If PixelValue = 0 Then
' デフォルト値
bReturn = _
SendMessage(Me.Handle, EM_SETTABSTOPS, 0, nTabwidth)
Else
' 指定値
bReturn = _
SendMessage(Me.Handle, EM_SETTABSTOPS, 1, nTabwidth)
End If
Me.Refresh()
' 復元処理
Me.Select(nStart, nLength)
If bReturn Then m_tabwidth = PixelValue
End If
End Sub
' ScrollToLine : 指定された行へ移動
Public Sub ScrollToLine(ByVal LineIndex As Integer)
Dim nLines As Integer = Me.GetLineCount()
If LineIndex <= 1 Then LineIndex = 0
If LineIndex >= nLines Then LineIndex = nLines - 1
Dim nChar As Integer = Me.GetCharIndex(LineIndex)
' 行の先頭文字にキャレットを移動
Me.Select(nChar, 0)
Me.ScrollToCaret()
End Sub
' ShowFileProperty : ファイルのプロパティの表示
Public Sub ShowFileProperty()
Try
' 構造体のメンバを指定
Dim udtInfo As ShellExecuteInfo
With udtInfo
.cbSize = Runtime.InteropServices.Marshal.SizeOf(udtInfo)
.fMask = SEE_MASK_INVOKEIDLIST
.hWnd = Me.Handle
.lpVerb = "properties"
.lpFile = m_pathname
End With
' プロパティを表示
Dim nValue As Integer = ShellExecuteEx(udtInfo)
Catch ex As Exception
MessageBox.Show(ex.Message, "プロパティ表示エラー", _
MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try
End Sub
Popupイベント発生時に、各メニューのオン/オフを判定しています。問題となっている"Text"プロパティ取得の代わりにLineCountとLineLengthを使って対応しています。Textが空の時は、LineCount=1、LineLength=0となります。この方法だとアンドゥデータが消えないようです。
ContextMenu_Clickイベントでは、各メニューのクリックイベントを一手に引き受けています。ひとつひとつ"..._Click"イベントを記述するよりもかなりすっきりします。
' ポップアップメニュー : ポップアップ
Private Sub m_popup_Popup(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles m_popup.Popup
Dim nLineCount As Integer = Me.GetLineCount
Dim nLineLength As Integer = Me.GetLineLength(0)
m_undo.Enabled = Me.CanUndo
m_cut.Enabled = Not Me.SelectedText.Equals("")
m_copy.Enabled = Not Me.SelectedText.Equals("")
m_delete.Enabled = Not Me.SelectedText.Equals("")
m_select.Enabled = Not (nLineCount + nLineLength).Equals(1)
m_paste.Enabled = _
Me.CanPaste(DataFormats.GetFormat(DataFormats.Text)) OrElse _
Me.CanPaste(DataFormats.GetFormat(DataFormats.Rtf))
End Sub
' ポップアップメニュー : クリックイベント
Private Sub ContextMenu_Click( _
ByVal sender As Object, ByVal e As EventArgs)
If sender Is m_undo Then : Me.Undo() : Me.ClearUndo()
ElseIf sender Is m_cut Then : Me.Cut()
ElseIf sender Is m_copy Then : Me.Copy()
ElseIf sender Is m_paste Then : Me.Paste()
ElseIf sender Is m_delete Then : Me.SelectedText = ""
ElseIf sender Is m_select Then : Me.SelectAll()
End If
End Sub