(Post 05/05/2006) dotNET cho phép ta tha hồ thừa kế, và bài này sẽ minh họa cách chúng ta sẽ 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. Hình 1 | |
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ư Hình 1 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ữ đ và Ð ta dùng d9 và D9. 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á^ảãạầ-ằ là -, 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á^ảãạầ-ằ là ^. Đ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: Tạo một DataObject Interface với Clipboard.GetDataObject() 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 vnTextbox và chươ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. (theo Vovisoft.com) |