隨著全球化程度加深,軟體越來越像蒲公英,到處飄散、紮根。這其中要解決的是不同語言的顯示問題。我們當然希望一套程式,可以不修改代碼就可以支援不同的語言,不要去維護很多的版本。
首先要談到的一個問題是亂碼問題,因為delphi win32到11.x版還是不支援unicode,所以一般使用Ansi碼,有這樣幾種情況會顯示亂碼:
- 使用的語言文字與系統當前設定的語言不一樣;比如簡體版QQ在繁體作業系統(或簡體作業系統的地區設定中“非Unicode程式的語言”設定為繁體)就是亂碼。即使改變Font.Charset,某些元件仍然會出現亂碼,如StatusBar。因此,在越南文版的windows顯示越南文,在伊朗文版的windows顯示伊朗文,不要在越南文版windows顯示伊朗文,在伊朗文版windows顯示越南文,這樣就能確保沒有亂碼問題。好在一般這樣的錯位用法也不多見。
- 系統沒有安裝你要顯示的語言的語言套件;
如果你要保證完全無亂碼,必須考慮使用unicode碼,使用成套的支援unicode的元件,如tnt,但它在UI變現上比較單一,你不可能不使用別的元件。
言歸正傳,首先,看看哪些地方的字串需要實現多語言,並來看看各種實現方法的優劣。
1、介面上的元件,如TButton的Caption;
2、主動彈出的訊息,如ShowMessage('Are you sure?'),Raise Exception.Create('Error!');
3、例外錯誤舉發的報告資訊,如f/0引起的exception;
4、第3方元件包內部的上述字串;
實現多語言的方法很多,列舉一二:
1、delphi內建的Resource產生工具
此工具把專案的dfm檔案裡的所有字串以及pas中定義為ResourceString的字串列舉出來,按不同的語言編譯成不同的Resource,專案編譯前先選語言,每種語言編譯成一個exe。
這個工具使用很不方便,不是一個完整的解決方案,跟Borland的Midas的demo一樣(TClientDataSet通過ProviderName串連到RemoteDatamodule的TDataSetProvider,實際開發Erp系統時,誰會放100個TDataSetProvider串連到100個TDataSet?),只是一個原理尚通的示範。
首先,由於dfm本身也是資源檔的一部分,因此每次修改都要“Update Resource DLLs...”,如修改Button1為Button2,如果你忘了,運行時就會報“找不到資源Button1的錯誤”;提供的字典編輯畫面中,出了字串,還有Left/Top等資料;字典不能重用,在一個模組翻譯了,在第2個模組還要再翻譯相同的詞。
其次,每種語言一個exe/bpl,如果你的系統是Package切割,bpl也是每種語言一個,還要小心別把不同語言的bpl組合在了一起,到時候一個畫面顯示中文,一個顯示德文(有一個可能是亂碼)就慘了。
再次,在作bpl組裝的系統時,第3方元件如果沒有提供多語言的方案,你就需要修改第3方元件,但一般我們不這樣幹,因為第3方元件會隨時更新,難道每次人家更新你也再更新人家。
因此,一般都沒有人使用Delphi本身提供的這個方案(除了作demo)。
2、Resource dll方式
用單獨的ResourceDll,用LoadResString等函數獲得翻譯字串,但你要到處寫這個函數來一一替換,特別是Form上的字串,噢,會累死人。字典可以重用。
3、網上討論很多的ini檔案方式
此方法是寫個替換的引擎,在運行時從ini檔案讀取語言字串來替換畫面元件的顯示文字。這個方法比第一種進步很多,不需要每種語言編譯一個exe了,只要提供不同的ini檔案就好;畫面修改時如果ini沒有同步更新也不會出現致命錯誤,最多就是某個文字沒有轉換;引擎也提供了字串轉換函式,因此也可以處理主動彈出的訊息。這個方法在檔案格式上有三種不同的實現:
(1)、[編號]=[字串]
每個字串從1開始編號,1,2,3,4......,很麻煩,代碼要修改,當然運行時切換語言沒問題。
(2)、[元件.屬性]=[字串]
這種實現把元件instance一一對應,用RTTI來判斷屬性,替換很精確,也可以運行時切換語言。不足之處是,略顯呆板,多個元件相同的字串會多次列出;沒有擴充性,表現TListView的Columns等複雜元件時比較吃力。
(3)、[舊字串]=[新字串]
不管元件的instance,ini是純粹的語言對照表,或者叫字典,擴充性、運行時切換語言可能在引擎裡。不足之處是不能處理一詞多義。
總的說來,這種方式有很大進步,但為了用ini檔案,大家還要費力的破解64k的限制,更專業的方式是使用自訂的檔案格式。
在簡單性方面,無疑是這種自訂的轉換引擎,[舊字串]=[新字串]的檔案格式來得方便,藉助字典管理工具,字典檔案可以重複使用,也可以提供給專業翻譯公司翻譯。那麼剩下的問題在引擎上,如何方便,最好使用者不寫一行代碼;如何擴充性強,支援任意的第三方元件;如何有彈性,同一個畫面有多種語言的文字,同一個詞可以轉換成不同的意思......
4、給每個元件類繼承一個子類,在子類的Loaded方法裡轉換文字。由於要處理的都是葉級元件(雖然TLabel、TPanel都是從TCustomControl來,但不能只處理TCustomControl),工作量比較大;對舊有程式除了換元件無能為力。
5、為每個元件類註冊一個轉換函式,引擎遍曆Container,為每個元件找到血源最近的轉換函式,調用這個函數轉換這個元件的文字。這樣可以不必處理葉結點,只需在恰當的元件層上註冊函數;不必改動舊有程式。設計時Form上只需要放一個轉換元件,這個元件在Loaded後開始掃描Form上的元件,從for I:=0 to ComponentCount-1或從for i:=0 to ControlCount-1遞歸,找到一個元件就去尋找其血緣最近的註冊函數,然後調用這個函數替換其文字。因爲註冊函數是額外加上去的,所以不會動到舊的代碼,對任意第方元件都可以擴展支援,且也不用去修改人家第3方元件的代碼。
我認為第5種方法很優雅,看起來比較乾淨。用GOF的設計模式來套,這屬於Mediator pattern(中介者模式)。多年前,我們使用一個叫TXPMenu的元件來獲得XP風格的介面,也是感覺到它很乾淨,一個元件就搞定一切,不用TLabel換成TFlatLabel,TButton換成TFlatButton......我記得《程式員》上還有文章專門稱讚這個元件。但那個元件沒有使用中介者模式,不能很好的擴充對第3方元件的支援。
最後,我們暢想一下,如果我是Borland,如何在Delphi裡完整支援多語言。Delphi提供了一個區塊定義的關鍵字“ResourceString”,在這個區塊定義的字串常量,編譯器會把它編譯在exe檔案的資源區,運行時用LoadStringA這個Windows API來讀取,因此有些外部轉換工具可以直接從exe檔案讀取這些資源字串,再寫入轉換後的字串;內嵌的轉換引擎也可以攔截這個API函數來轉換文字。但是如果exe裡的字串資源化不徹底,就無能為力,這個不徹底恰恰來自Delphi的DFM檔案,Delphi把DFM檔案整個作為一項資源放在exe裡,其上的字串就沒法決定是否要don`t resource(Delphi源碼裡很多常量字串都有這個提示)了。
如果除了string,widestring,ansistring等等這些資料類型,delphi增加一種資料類型multistring,然後修改vcl元件定義(拜託Borland連同Unicode一起解決了吧),像TLabel.Caption定義成MultiString,對MultiString類型,有一種專門的處理方法,如類似ResourceString用LoadString API來處理,每次讀取就轉換一次,但應該比這個內容更多,比如要傳出instance,然後提供一個全域的ApplicationMulti元件,類似ApplicationEvent,讓外面能捕捉到。至於字典,只能外部使用者提供(當然可以制定一個標準格式讓delphi人都可以共用交換)。
此法看起來可行,但還有個效率問題要考慮,(1)每次讀取都轉換,對頻繁draw的東西效率低;(2)比如一個ToolBar有好多的ToolButton,批次更新時一般都會用BeginUpdate/EndUpdate,vcl如何告知後代來提高這種效率。(補記:效率看起來不是問題,對多次字串更改導致頻繁draw,其實元件自己已經會用beginupdate/endupdate處理,外部不會涉及)