Logo of Vovisoft

Thừa kế Textbox để đánh chữ Việt Unicode

Lập trình dùng thừa kế

Hết rồi giai đoạn bực mình với VB6 thì sự giới hạn về lập trình theo hướng đối tượng (Object Oriented), .NET cho phép ta tha hồ thừa kế. Do đó, một trong những dự án nho nhỏ đầu tiên của chúng ta là thừa kế từ Textbox bình thường để tạo một Textbox, tạm gọi là vnTextbox, hỗ trợ đánh chữ Việt Unicode theo lối VNI hay VIQR. Dĩ nhiên, ta vẫn tiếp tục giữ các programs bỏ dấu chuyên nghiệp ưng ý của mình như VietKey, UniKey, VPSKey, .v.v.., nhưng có thể sau nầy sẽ có trường hợp ta cung cấp cho khách hàng một chương trình áp dụng tiếng Việt để họ dùng cho nhu cầu chuyên môn mà không cần phải dùng thêm một program bỏ dấu hỗ trợ.

Để tạo một Control thừa kế từ Textbox bạn khởi động một Project mới loại Windows Control Library như sau:

Kế đó, khi mở code ra thay thế hai hàng:

Public Class UserControl1 
   Inherits System.Windows.Forms.UserControl 
bằng hai hàng sau:

Public Class vnTextbox 
   Inherits System.Windows.Forms.TextBox 

Đánh dấu theo lối VNI

Ðể đánh dấu cho các nguyên âm chữ Việt, trong vnTextbox ta tạm dùng phương pháp VNI. Tức là ta đánh nguyên âm trước, kế đó ta đánh một con số từ 1 đến 9 để bỏ dấu.

Các con số 1..6 theo sau chữ a chẳng hạn, sẽ cho ta các chữ á à ả ã ạ â; số 7 theo sau chữ u sẽ cho ta ư; số 8 theo sau chữ a sẽ cho ta ă; số 9 theo sau chữ d sẽ cho ta đ. Để bỏ hai dấu thì ta dùng hai con số, thí dụ a36 thì sẽ đuợc hiển thị thành , còn u27 thì sẽ cho . Ðể đánh các chữ đÐ ta dùng d9D9. Chắc chắn bạn sẽ thấy program nầy đơn sơ quá, nhưng nó sẽ dễ hiểu, và sau đó, nếu thích bạn có thể thêm thắt các chức năng.

Để bỏ dấu theo lối VIQR thì thay vì các con số 1,2,3,4,5,6,7,8,9 ta dùng ' ` ? ~ . ^ + (hay *) ( d (hay -). Ðặc biệt control vnTextbox nầy dùng gần như hoàn toàn look-up table để tính ra các nguyên âm có dấu. Trước hết, mỗi khi user đánh một con số từ 1 đến 9, thì program nhìn xem character phía trước cursor (gọi là LastCh) là chữ gì. Kế đó nó tìm đến hàng chữ chứa toàn bộ những nguyên âm có thể thay thế LastCh, tùy theo con số mà user vừa đánh vào. Ở đây kể cả trường hợp user vừa đánh một Backspace.

Cái bảng chứa những hàng chữ ấy đuợc chứa trong một array-of-string tên ChList và nó được initialised trong Constructor Sub New của vnTextbox như dưới đây:

Private ChList(148) As String  ' List of character groups like "aáàảãạâ-ă"
ChList(0) = "aáàảãạăắằẳẵặâấầẩẫậeéèẻẽẹêếềểễệiíìỉĩịoóòỏõọôốồổỗộơớờởỡợuúùủũụưứừửữựyýỳỷỹỵdđAÁÀẢÃẠĂẮẰẲẴẶÂẤẦẨẪẬEÉÈẺẼẸÊẾỀỂỄỆIÍÌỈĨỊOÓÒỎÕỌÔỐỒỔỖỘƠỚỜỞỠỢUÚÙỦŨỤƯỨỪỬỮỰYÝỲỶỸỴD" 
ChList(1) = "aáàảãạâ-ă" 
ChList(2) = "a^àảãạấ-ắ" 
ChList(3) = "aá^ảãạầ-ằ" 
ChList(4) = "aáà^ãạẩ-ẳ" 
ChList(5) = "aáàả^ạẫ-ẵ" 
ChList(6) = "aáàảã^ậ-ặ" 
ChList(7) = "aắằẳẵặâ-^" 
ChList(8) = "ăắằẳẵặấ-ắ" 
ChList(9) = "ăắằẳẵặầ-ằ" 
ChList(10) = "ăắằẳẵặẩ-ẳ" 
ChList(11) = "ăắằẳẵặẫ-ẵ" 
ChList(12) = "ăắằẳẵặậ-ặ" 
ChList(13) = "aấầẩẫậ^-ă" 
ChList(14) = "âấầẩẫậấ-ắ" 
ChList(15) = "âấầẩẫậầ-ằ" 
ChList(16) = "âấầẩẫậẩ-ẳ" 
ChList(17) = "âấầẩẫậẫ-ẵ" 
ChList(18) = "âấầẩẫậậ-ặ" 
ChList(19) = "eéèẻẽẹê" 
ChList(20) = "e^èẻẽẹế" 
ChList(21) = "eé^ẻẽẹề" 
ChList(22) = "eéè^ẽẹể" 
ChList(23) = "eéèẻ^ẹễ" 
ChList(24) = "eéèẻẽ^ệ" 
ChList(25) = "eếềểễệ^" 
ChList(26) = "êếềểễệế" 
ChList(27) = "êếềểễệề" 
ChList(28) = "êếềểễệể" 
ChList(29) = "êếềểễệễ" 
ChList(30) = "êếềểễệệ" 
. . .
ChList(0) chứa toàn bộ các nguyên âm. Tương ứng với mỗi nguyên âm (LastCh) trong ChList(0) là một hàng chứa tất cả mọi chữ có thể đuợc dùng để thay thế LastCh khi user đánh vào một con số 1..9 hay Backspace.
Thí dụ nếu LastCh là à, ta sẽ dùng ChList(3), nó chứa các chữ: aá^ảãạầ-ằ
Kế đó nếu user đánh số 3 ta sẽ thay thế dấu sắc thành dấu hỏi để có chữ . Còn nếu thay vì đánh số 3, user đánh số 8, thì ta sẽ có chữ , tức là thêm dấu ă cho chữ à.
Nếu user đánh thêm một số 7 thì character tướng ứng với số 7 trong hàng aá^ảãạầ-ằ-, hể gặp character - thì ta làm ngơ.
Nếu user đánh thêm một số 2 sau chữ à bạn sẽ thấy character tướng ứng với số 2 trong hàng aá^ảãạầ-ằ^. Điều nầy nhắc ta biết là user đánh a22, nên ta sẽ hiển thị a2.
Nếu user đánh Backspace, thay vì một con số, ta sẽ dùng nguyên âm nằm ở đầu dòng, tức là chữ a. Như thế nếu LastCh là , thì sau một Backspace ta có , sau thêm một Backspace kế tiếp ta sẽ còn lại a.

Kỹ thuật Program dùng để thay thế LastCh là select (highlight) LastCh rồi Paste nguyên âm mới.

Dưới đây là Listing của Function GetToneCharPos() để trả về một giá trị từ 1 đến 9 tượng trưng cho dấu:

Private Function GetToneCharPos( ByVal KeyChar As Integer) As Integer 
  ' If Typing stype is VNI, see if user enters "1".."9" or "d"
  ' If so return 1..9 and also 9 for "d". Otherwise return -1
  '
  ' If Typing stype is VIQR, return 1..9 for characters '`?~.^+(d . Otherwise return -1
  ' We also allow for * and - to be same as + and d successively.
  ' i.e. u+ or u* and dd or d- are OK.
   GetToneCharPos = -1 
   If mTypingStyle = "VNI" Then 
      If (KeyChar = 68) Or (KeyChar = 100) Then ' ie. "d" for dd or DD
         GetToneCharPos = 9 
      ElseIf (KeyChar >= &H31) And (KeyChar <= &H39) Then 
        ' it's a digit. KeyChar of "1" is &H31
         GetToneCharPos = KeyChar - &H30 
      End If 
   ElseIf mTypingStyle = "VIQR" Then 
      Console.WriteLine("KeyChar:{0}", KeyChar) 
      Select Case KeyChar 
      Case 39 '
         GetToneCharPos = 1 ' '
      Case 96 
         GetToneCharPos = 2 ' `
      Case 126 
         GetToneCharPos = 4 ' ~
      Case 63 
         GetToneCharPos = 3 ' ?
      Case 46 
         GetToneCharPos = 5 ' .
      Case 94 
         GetToneCharPos = 6 ' ^
      Case 43, 42 ' + or *
         GetToneCharPos = 7 
      Case 40 
         GetToneCharPos = 8 ' (
      Case 100, 68, 45 ' d D or -
         GetToneCharPos = 9 
      End Select 
   End If 
End Function 
Trong Control vnTextbox ta không thể để code hỗ trợ đánh dấu chữ Việt trong Sub vnTextbox_KeyDown hay Sub vnTextbox_KeyUp được vì một khi KeyDown hay KeyDown Events đã được raised rồi ta không thể bỏ qua Keystroke hay thay đổi trị giá của nó thành 0 như trong VB6. Do đó, ở đây ta Override Function ProcessKeyMessage. Nếu giá trị trả về (Returned value) của hàm ProcessKeyMessage là True thì ta ngăn cản không cho Keyboard Event xẩy ra. Loại Event có thể xẩy ra sau đó tùy thuộc vào trị số của m.Msg. Trong hàm ProcessKeyMessage, ta chỉ xử lý thông điệp m.Msg = KeyUp (có giá trị 258). Nếu m.Msg là cho KeyDown hay KeyPress thì ta làm ngơ và cho ProcessKeyMessage return False để KeyDown hay KeyPress events xẩy ra như bình thường.

Protected Overrides Function ProcessKeyMessage( ByRef m As System.Windows.Forms.Message) As Boolean 
  ' Get out if this is not a KeyUp message
   If m.Msg <> 258 Then Return False 
   Const Delay As Integer = 100 
  ' Obtain the Keystroke character
   Dim KeyChar As Integer = m.WParam.ToInt32 
  ' Process a keystroke
   Dim Pos, ToneCharPos, Offset As Integer 
   Dim NewCh As String 
   If KeyChar = 8 Then ' It's a backspace character
      If Me.SelectionStart = 0 Then Return True 
     ' Obtain the position of the line containing all possible modified characters
      Pos = GetLastCharMapPos() 
     ' Select the character just on the left of the cursor
      Me.SelectionStart -= 1 
      Me.SelectionLength = 1 
      If Pos > 0 Then 
         If LastCh <> ChList(Pos).Substring(0, 1) Then 
           ' Get here if backspace means removing ^ or ', ` etc..
           ' Copy the new (modified) character to clipboard, it's the leftmost
           ' character on the line
            Clipboard.SetDataObject(ChList(Pos).Substring(0, 1)) 
           ' Paste it to replace the character on the left
            Me.Paste() 
            ConcatCharacterIfFailed(ChList(Pos).Substring(0, 1)) 
         Else 
           ' get here if it's a genuine backspace
            Me.Cut() 
         End If 
      Else 
        ' Select the character just on the left of the cursor
         Me.Cut() 
      End If 
     ' Swallow the actual keystroke
      Return True 
   ElseIf KeyChar = 92 Then ' it's a back slash \ which is the Escape character
      If Not EscapeFlag Then 
         EscapeFlag = True ' Set the Escape flag
         Return True ' Swallow the actual keystroke
      End If 
   Else 
     ' Map the key entered (i.e: "1".."9", "d", or "'", "`" , "?" , "~", ".", "+", "(" ) 
     ' to the position 1..9, to be used for selecting the new character from a string like:
     ' eg: a a' a` a? a~ a. a^ - a(
      ToneCharPos = GetToneCharPos(KeyChar) 
      If ToneCharPos > 0 Then 
        ' Get here if a digit in range 1..9 has been entered
        ' Ignore the digit if Escape Flag is set
         If Not EscapeFlag Then 
           ' Obtain the position of the line containing all possible modified characters
            Pos = GetLastCharMapPos() 
           ' If Pos = 0 then simply display the character, ie. leave the keystroke alone
            If Pos > 0 Then 
              ' Work out the offset of the new character on the line
               Offset = ToneCharPos + 1 
               If Offset <= ChList(Pos).Length Then 
                 ' Extract the new character
                  NewCh = ChList(Pos).Substring(Offset - 1, 1) 
                 ' Console.WriteLine("ChList:{0} Pos:{1} Offset-1:{2}", ChList(Pos), Pos, Offset - 1)
                  If NewCh = "^" Then ' roll back last character
                    ' eg: letter a followed by 11 => á1 to become a1
                    ' Select the character just on the left of the cursor
                     Me.SelectionStart -= 1 
                     Me.SelectionLength = 1 
                    ' Copy leftmost character on the line to clipboard
                     Clipboard.SetDataObject(ChList(Pos).Substring(0, 1)) 
                    ' Paste it to replace the character on the left
                     Me.Paste() 
                     ConcatCharacterIfFailed(ChList(Pos).Substring(0, 1)) 
                    ' Leave keystroke alone to let it fall through and be displayed
                    ' Forget it if the new character is "-", let the keystroke displayed
                  ElseIf NewCh <> "-" Then 
                    ' Select the character just on the left of the cursor
                     Me.SelectionStart -= 1 
                     Me.SelectionLength = 1 
                    ' Copy the new (modified) character to clipboard
                     ToClipboard(NewCh) 
                    ' Paste it to replace the character on the left
                     Me.Paste() 
                     ConcatCharacterIfFailed(NewCh) 
                     Return True ' Swallow the actual keystroke
                  End If 
               End If 
            End If 
         End If 
      End If 
   End If 
   EscapeFlag = False ' Reset the Escape Flag
End Function 
Để vnTextbox hỗ trợ hai lối bỏ dấu, VNI và VIQR, ta cho nó Property TypingStyle như sau:

Property TypingStyle() As String 
   Get 
      Return mTypingStyle 
   End Get 
   Set ( ByVal Value As String) 
     ' Chỉ chấp nhận "VNI" hay "VIQR" mà thôi
      If Value = "VNI" Or Value = "VIQR" Then 
         mTypingStyle = Value 
      End If 
   End Set 
End Property 
Lưu ý cách copy một character vào Clipboard:

' Copy it to clipboard
Clipboard.SetDataObject(Ch) 
Việc lấy Unicode text string từ Clipboard gồm có hai bước:
  1. Tạo một DataObject Interface với Clipboard.GetDataObject()
  2. Dùng Interface ấy để lấy data từ Clipboard dưới dạng Unicode (DataFormats.UnicodeText), rồi đổi nó ra string
' Create a new instance of the DataObject interface.
Dim data As IDataObject = Clipboard.GetDataObject() 
' Retrieve LastCh the character in Unicode Text format from clipboard
LastCh = data.GetData(DataFormats.UnicodeText).ToString() 
Chắc bạn đã để ý, chữ Việt ta dùng ở đây là Unicode, mặc dầu ta không nhắc gì đến encoding của nó hay code point của mỗi character. Nếu bạn chưa quen với Unicode cho chữ Việt thì hãy đọc bài Dùng Unicode chữ Việt trong .NET

Rất tiếc vì method Paste bên trong vnTextbox không reliable (bất chừng), nhiều khi nó không Paste được nên LastCh vẫn còn bị highlighted. Trong trường hợp đó Sub ConcatCharacterIfFailed sẽ dùng string concatenation (ghép strings) để khắc phục khó khăn. Nhưng cách ghép strings không hiệu năng bằng Paste.

Control vnTextbox có hỗ trợ Escape character \. Con số theo sau Escape character \ sẽ không bị dùng vào việc bỏ dấu cho nguyên âm ngay trước đó.

Xin lưu ý: Ðể program nầy chạy bỏ dấu đuợc bạn phải tạm thời ngưng các programs như VPSkey hay VietKey, UniKey .v.v.. Lý do là các programs kia sẽ giựt trước các keystrokes của những con số 1 đến 9, sau khi kiểm tra rồi không chịu buông ra cho program nầy thấy.

Bạn có thể tải về mã nguồn của control vnTextboxchương trình thử. Hai Zip files nầy là cho 2 projects. Bạn load vnTextbox và compile trước để dùng WindowsControlLibrary2.dll trong bin folder làm reference cho project TestvnTextbox. Sau khi load TestvnTextbox, bạn hãy remove WindowsControlLibrary2 từ References của nó rồi dùng Menu Command Project |Add Reference để refernce library WindowsControlLibrary2.dll vừa mới compile.

Nếu bao giờ IDE than phiền về Licence bạn chỉ cần delete file licences.licx trong Project Folder.
Dưới đây là hình form chính của program TestvnTextbox.
  Học Microsoft .NET