類的概念在許多語言中出現,很容易理解。它將數據和操作進行封裝,以便將來的復用。
模塊,在Python可理解為對應于一個文件。在創建了一個腳本文件后,定義了某些函數和變量。你在其他需要這些功能的文件中,導入這模塊,就可重用這些函數和變量。一般用module_name.fun_name,和module_name.var_name進行使用。這樣的語義用法使模塊看起來很像類或者名字空間,可將module_name 理解為名字限定符。模塊名就是文件名去掉.py后綴。下面演示了一個簡單的例子:
#moduel1.py
def say(word):
print word
#caller.py
import module1
print __name__
print module1.__name__
module1.say('hello')
$ python caller.py __main__ module1 hello
例子中演示了從文件中調用模塊的方法。這里還展示了一個有趣的模塊屬性__name__,它的值由Python解釋器設定。如果腳本文件是作為主程序調用,其值就設為__main__,如果是作為模塊被其他文件導入,它的值就是其文件名。這個屬性非常有用,常可用來進行模塊內置測試使用,你會經常在一些地方看到類似于下面的寫法,這些語句只在作為主程序調用時才被執行。
if __name__ == '__main__':
app = wxapp(0)
app.MainLoop()
上面的例子中,當module1被導入后,python解釋器就在當前目錄下尋找module1.py的文件,然后再從環境變量PYTHONPATH尋找,如果這環境變量沒有設定,也不要緊,解釋器還會在安裝預先設定的的一些目錄尋找。這就是在導入下面這些標準模塊,一切美好事情能發生的原因。
import os import sys import threading ...
這些搜索目錄可在運行時動態改變,比如將module1.py不放在當前目錄,而放在一個冷僻的角落里。這里你就需要通過某種途徑,如sys.path,來告知Python了。sys.path返回的是模塊搜索列表,通過前后的輸出對比和代碼,應能理悟到如何增加新路徑的方法了吧。非常簡單,就是使用list的append()或insert()增加新的目錄。
#module2.py import sys import os print sys.path workpath = os.path.dirname(os.path.abspath(sys.argv[0])) sys.path.insert(0, os.path.join(workpath, 'modules')) print sys.path
$ python module2.py ['e:\\Project\\Python', 'C:\\WINDOWS\\system32\\python25.zip', ...] ['e:\\Project\\Python\\modules', 'e:\\Project\\Python', 'C:\\WINDOWS\\system32\\python25.zip', ...]
模塊能像包含函數定義一樣,可包含一些可執行語句。這些可執行語句通常用來進行模塊的初始化工作。這些語句只在模塊第一次被導入時被執行。這非常重要,有些人以為這些語句會多次導入多次執行,其實不然。
模塊在被導入執行時,python解釋器為加快程序的啟動速度,會在與模塊文件同一目錄下生成.pyc文件。我們知道python是解釋性的腳本語言,而.pyc是經過編譯后的字節碼,這一工作會自動完成,而無需程序員手動執行。
在創建許許多多模塊后,我們可能希望將某些功能相近的文件組織在同一文件夾下,這里就需要運用包的概念了。包對應于文件夾,使用包的方式跟模塊也類似,唯一需要注意的是,當文件夾當作包使用時,文件夾需要包含__init__.py文件,主要是為了避免將文件夾名當作普通的字符串。__init__.py的內容可以為空,一般用來進行包的某些初始化工作或者設置__all__值,__all__是在from package-name import *這語句使用的,全部導出定義過的模塊。
PO文件是Portable Object文件的簡稱,它包含需要翻譯的字符串。我們需要從源文件進行提取。首先,對源文件test.py編輯,標識代碼里需要翻譯的字符串內容。我們使用_("xx")的方法,這種形式可能在許多開源源代碼中見識過。
#加載菜單欄
menubar = rc.LoadMenuBar('IDR_MENU')
這里的IDR_MENU是資源標識ID,不需要翻譯,因此不做改變,而下面的代碼:
info.SetVersion('1.0')
info.SetDescription('XRC i18n Demo')
'XRC i18n Demo'是描述性的文本,需要進行翻譯,將需要處理為
info.SetVersion('1.0')
info.SetDescription(_('XRC i18n Demo'))
接著需要生成.pot(Portable Object Template),這是po的模板文件。在將來程序可能配置成其他語種,其他語言的po文件都從它而來。為了創建這文件,需要用到GNU gettext工具集中的xgettext。向xgettext傳入些必要的信息,來創建.pot文件。
>xgetttext --output=test.pot test.py
我們將wxPython界面以XRC文件保存了,那里同樣有要翻譯的字符串需要提取。用XRCed工具將XRC生成python代碼,勾選上'Generate gettext strings'項即可。將源文件和XRC生成的test_xrc.py文件一起處理,生成一個test.pot。
>xgettext --output=test.pot test.py test_xrc.py
將得到的test.pot另存為test.po文件,然后進行翻譯編輯,在這過程中文件需要使用utf-8編碼。將對應的英文翻譯成中文,將charset更改為utf-8。
"Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" #: test.py:19 msgid "XRC i18n Demo" msgstr "XRC 國際化示例" ...
.pot和.po這些文件都是文本文件,主要供翻譯者使用。為了使程序在運行時能獲取相關的翻譯的內容,要進行所謂的編譯過程,將文本文件轉換為二進制文件.mo。這里用了gettext工具集中的另一程序msgfmt。
> msgfmt --output=test.mo test.po
因為windows下沒有像linux像有公共存儲.mo文件的目錄,保持平臺的遷移性,在應用程序本地目錄下新建locale目錄,用來存放編譯過的.mo文件,然后將test.mo移動至locale目錄。在完成這些步驟后,就轉入代碼方面的更改了。
原先的代碼只需要做小改動:
def OnInit(self):
wx.Locale.AddCatalogLookupPathPrefix('locale')
self.locale = wx.Locale(wx.LANGUAGE_CHINESE_SIMPLIFIED)
self.locale.AddCatalog('test')
import __builtin__
__builtin__.__dict__['_'] = wx.GetTranslation
首先,增加了新的目錄文件路徑,這將使wxPython搜索這個目錄,尋找匹配的.mo文件。接著創建wx.Locale對象,將其初始化為簡體中文,這將對應于zh_CN。最后將wx.GetTranslation做了一全局映射,這樣你在其他類中,比如示例中TestFrame也能使用_('xx')調用。這樣wxPython的i18n工作就完成了,下面是翻譯前后的界面截圖。
如果你將locale目錄下的test.mo文件刪除掉,然后將test.py中的wx.LANGUAGE_CHINESE_SIMPLIFIED改為wx.LANGUAGE_CHINESE,重新運行程序看看。發現界面變成了如下的繁體中文,但是菜單'檔案'下的Exit還是英文。
因為缺失.mo文件,但又指定wx.LANGUAGE_CHINESE,wxPython運行時使用了wxstd.mo文件。wxstd.mo有許多預編譯好的常見字符串的對應關系,它隨wxPython發布,在wx/locale下有許多語言版本的wxstd.mo。
對于wxPython會對待查目錄"DIR"來搜索.mo文件,查找它下面的這些目錄,(DIR/LANG/LC_MESSAGES;DIR/LANG;DIR),對于哪些是待查目錄,各個系統下又有不同,在所有的平臺上,LC_PATH環境變量指定的目錄將成為待查目錄,在Linux下/share/locale, /usr/share/locale, /usr/lib/locale, /usr/locale /share/locale以及當前目錄將是待查目錄。在上面我們已經用過AddCatalogLookupPathPrefix()函數,其作用就是增加自己的待查目錄。
在示例程序中,將test.mo放在locale\zh_CN\LC_MESSAGES或者locale\zh\LC_MESSAGES同樣是可行的。但是如果使用wx.LANGUAGE_CHINESE指定,則zh_CN目錄將不可行,因為它只是特化目錄,指簡體中文,而zh目錄同樣適用。
gettext進行國際化是開源社區的主流方案,它也提供了許多實用工具供使用。上面提到了xgettext,msgfmt,還有msginit.exe,它將根據.pot文件創建新的.po文件,然后初始化一些元信息,像作者信息,項目,以及編碼等,當然也可像上面的手工編輯。msgmerge.exe將兩個.po文件進行合并。除了使用GNU Gettext工具集,也可使用隨python發布的tool\i18n目錄下pygettext.py和msgfmt.py,它們等同于上述的兩個工具。
對于編輯.po文件,可以嘗試一下Poedit,它提供了圖形化的編輯環境,其他功能我就不清楚了。
但在找到這篇文章之前,和編寫插件的過程中,還是費了很多功夫。最早我認為寫插件是需要下載SDK之類的軟件,所以在Live Writer官方開發網站,Live Writer網,MSDN之類的找了個遍,看見是有SDK之類字樣的下載,但弄不下來只有文檔,根本不見其什么頭文件,DLL之類的。在這花費了很多時間,最后才發現WLW插件的SDK是隨WLW一起分發了,也就是WindowsLive.Writer.Api.dll之類的,這些dll 都隨WLW主程序在一個目錄中。還有一點是,現在WLW在中國是隨Live套件一起發布的,因此路徑由原來的C:\Program Files\Windows Live Writer變成了C:\Program Files\Windows Live\Writer,插件目錄為Plugin。如果在網上發現有好用的插件,只需要將其發布的插件dll扔到這個目錄就行了。
在開發中碰到圖標資源不能成功加載,在Dflying Chen的文章中特意提到了圖標資源需要是嵌入形式,我也按照其操作的,總以為是這里出現問題。后來花了一些時間,才找到總是的根源:自己在開發中更改了工程名,導致最后生成的程序集的名稱與后來的命名空間名稱不一致,圖標路徑就出錯了。C#也只是這次用一下,這些都沒有接觸到。
昨天弄完自己的“插入Pre標記”插件后,想到自己寫博客常需要截圖,遂想再開發一個截圖工具的。最早搜到了別人調用SnagIt搜件,不好用,因為SnagIt是商業軟件,需要注冊的。后來找到了picpick,小巧免費,非常實用。我想調用picpick的,但是在參照Insert SnagIt Screen Capture發現是用COM接口,而無奈picpick沒有這樣供開發使用的接口考慮,最終不可行。后來經過一些其他的嘗試,都告失敗。最后還是搜索幫了忙,找到了Screen Capture這個插件,原來有別人已完工了。
最后附上,我用這個新插件截的圖,非常好用,只需一步:
一旦線程對象被創建,它的活動需要通過調用線程的start()方法來啟動。這方法再調用控制線程中的run方法。
一旦線程被激活,則這線程被認為是'alive'(活動)。當它的run()方法終止時-正常退出或拋出未處理的異常,則活動狀態停止。isAlive()方法測試線程是否是活動的。
一個線程能調用別的線程的join()方法。這將阻塞調用線程,直到擁有join()方法的線程的調用終止。
線程有名字。名字能傳給構造函數,通過setName()方法設置,用getName()方法獲取。
線程能被標識為'daemon thread'(守護線程).這標志的特點是當剩下的全是守護線程時,則Python程序退出。它的初始值繼承于創建線程。這標志用setDaemon()方法設置,用isDaemon()獲取。
存在'main thread'(主線程),它對應于Python程序的初始控制線程。它不是后臺線程。
有可能存在'dummy thread objects'(啞線程對象)被創建。這些線程對應于'alien threads'(外部線程),它們在Python的線程模型之外被啟動,像直接從C語言代碼中啟動。啞線程對象只有有限的功能,它們總是被認為是活動的,守護線程,不能使用join()方法。它們從不能被刪除,既然它無法監測到外部線程的中止。
構造函數能帶有關鍵字參數被調用。這些參數是:
group 應當為 None,為將來實現ThreadGroup類的擴展而保留。
target 是被 run()方法調用的回調對象. 默認應為None, 意味著沒有對象被調用。
name 為線程名字。默認,形式為'Thread-N'的唯一的名字被創建,其中N 是比較小的十進制數。
args是目標調用參數的tuple,默認為()。
kwargs是目標調用的參數的關鍵字dictionary,默認為{}。
如果子線程重寫了構造函數,它應保證調用基類的構造函數(Thread.__init__()),在線程中進行其他工作之前。
在每個線程對象中最多被調用一次。它安排對象的run() 被調用在一單獨的控制線程中。
run()
你可能在子類重寫這方法。標準的 run()方法調用作為target傳遞給對象構造函數的回調對象。如果存在參數,一系列關鍵字參數從args和kwargs參數相應地起作用。
當timeout參數未被設置或者不是None,它應當是浮點數指明以秒計的操作超時值。因為join()總是返回None,你必須調用isAlive()來判別超時是否發生。
當timeout 參數沒有被指定或者是None時,操作將被阻塞直至線程中止。
線程能被join()許多次。
線程不能調用自身的join(),因為這將會引起死鎖。
在線程啟動之前嘗試調用join()會發生錯誤。
這名字是只用來進行標識目的的字符串。它沒有其他作用。多個線程可以取同一名字。最初的名字通過構造函數設置。
大致上,線程從 start()調用開始那點至它的run()方法中止返回時,都被認為是活動的。模塊函數enumerate()返回活動線程的列表。
初始值繼承至創建線程。
當沒有活動的非守護線程時,整個Python程序退出。
命令行接口是普遍,基礎的人機交互接口,從命令行提取程序的運行時選項的方法有很多。你可以自己編寫相對應的完整的解析函數,或許你有豐富的C語言編程經驗,熟知getopt()函數的用法,又或許使用Python的你已經在使用optparse庫來簡化這一工作。大家在平時不斷地談及到“不要重復造輪子”,那就需要掌握一些順手的庫,這里介紹一種C++方式來解析命令行選項的方法,就是使用Boost.Program_options庫。
program_options提供程序員一種方便的命令行和配置文件進行程序選項設置的方法。使用program_options庫而不是你自己動手寫相關的解析代碼,因為它更簡單,聲明程序選項的語法簡潔,并且庫自身也非常小。將選項值轉換為適合的類型值的工作也都能自動完成。庫有著完備的錯誤檢查機制,如果自己手寫解析代碼時,就可能會錯過對一些出錯情況的檢查了。最后,選項值不僅能從命令行獲取,也能從配置文件,甚至于環境變量中提取,而這些選擇不會增加明顯的工作量。
以下面簡單的hello程序進行說明,默認打印hello world,如果傳入-p選項,就會打印出人的姓名,另外通過傳入-h選項,可以打印出幫助選項。略微看一眼代碼文件和相應的屏幕輸入輸出,然后我們再一起來看看這些是如何發生的。
//hello.cpp
#include <iostream>
#include <string>
#include <boost/program_options.hpp>
using namespace std;
int main(int argc, char* argv[])
{
using namespace boost::program_options;
//聲明需要的選項
options_description desc("Allowed options");
desc.add_options()
("help,h", "produce help message")
("person,p", value<string>()->default_value("world"), "who")
;
variables_map vm;
store(parse_command_line(argc, argv, desc), vm);
notify(vm);
if (vm.count("help")) {
cout << desc;
return 0;
}
cout << "Hello " << vm["person"].as<string>() << endl;
return 0;
}
下面是在Windows命令提示符窗口上的輸入輸出結果,其中">"表示提示符。
>hello Hello world >hello -h Allowed options: -h [ --help ] produce help message -p [ --person ] arg (=world) who >hello --person len Hello len
首先通過options_description類聲明了需要的選項,add_options返回了定義了operator()的特殊的代理對象。這個調用看起來有點奇怪,其參數依次為選項名,選項值,以及選項的描述。注意到示例中的選項名為"help,h",是因為聲明了具有短選項名和長選項名的選項,這跟gnu程序的命令行具有一致性。當然你可以省略短選項名,但是這樣就不能用命令選項簡寫了。第二個選項的聲明,定義了選項值為string類型,其默認值為world.
接下來,聲明了variables_map類的對象,它主要用來存儲選項值,并且能儲存任意類型的值。然后,store,parse_command_line和notify函數使vm能存儲在命令行中發現的選項。
最后我們就自由地使用這些選項了,variables_map類的使用就像使用std::map一樣,除了它必須用as方法去獲取值。如果as方法調用的指定類型與實際存儲的類型不同,就會有異常拋出。
具有編程的你可能有這樣的經驗,使用cl或gcc對源文件進行編譯時,可直接將源文件名放置在命令行中,而無需什么選項字母,如gcc a.c之類的。prgram_options也能處理這種情況,在庫中被稱為"positional options"(位置選項),但這需要程序員的一點兒幫助才能完成。看下面的經過對應修改的代碼,我們無需傳入"-p"選項,就能可指定"person"選項值
positional_options_description p;
p.add("person", -1);
store(command_line_parser(argc, argv).options(desc).positional(p).run(), vm);
>hello len Hello len
前面新增的兩行是為了說明所有的位置選項都應被解釋成"person"選項,這里還采用了command_line_parser類來解析命令行,而不是用parse_command_line函數。后者只是對前者類的簡單封裝,但是現在我們需要傳入一些額外的信息,所以要使用類本身。
一般來說,在命令行上指定所有選項,對用戶來說是非常煩人的。如果有些選項要應用于每次運行,那該怎么辦呢。我們當然希望能創建出帶有些常用設置的選項文件,跟命令行一起應用于程序中。當然這一切需要將命令行與配置文件中的值結合起來。比如,在命令行中指定的某些選項值應該能覆蓋配置文件中的對應值,或者將這些值組合起來。
下面的代碼段將選項通過文件讀取,這文件是文本格式,可用"#"表示注釋,格式如命令行中的參數一樣,選項=值
ifstream ifs("config.cfg");
store(parse_config_file(ifs,config),vm);
notify(vm);
Boost Lambda庫是C++模板庫,以C++語言實現了lambda抽象.Lambda這個術語來自函數編程語言和lambda閉包理論,lambda抽象實際上定義了匿名函數.了解過C#新引入的匿數函數特性或Lisp編程的人,對這些概念理解會有很大幫助.Lambda庫設計的主要動機是為STL算法提供靈活方便的定義匿名函數對象的機制.這個Lambda庫究竟是有什么用呢?代碼勝千言!看下面將STL容器中的元素打印到標準輸出上的代碼.
for_each(a.begin(), a.end(), std::cout << _1 << ' ');
表達式std::cout << _1 << ' '定義了一元函數對象.變量_1是函數的形參,是實參的占位符.每次for_each的迭代中,函數帶著實際的參數被調用,實際參數取代了占位符,然后函數體里的內容被執行.Lambda庫的核心就是讓你能像上面所展示的那樣,在STL算法的調用點,定義小的匿名函數對象.
Lambda庫只由頭文件組成,這就意味著你不需要進行任何編譯,連接,生成二進制庫的動作,只需要boost庫頭文件路徑包含進你的工程中即可使用.
與現代的C++語言一樣,在使用時你需要聲明用到的名字空間,把下列的代碼包含在你的源文件頭:
using namespace boost::lambda;
在標準模板庫STL成為標準C++的一部分后,典型的STL算法對容器中元素的操作大都是通過函數對象(function objects)完成的.這些函數作為實參傳入STL算法.
任何C++中以函數調用語法被調用的對象都是函數對象.STL對某些常見情況預置了些函數對象.比如:plus,less,not1下面就是標準plus模板的一種可能實現:template <class T>
struct plus : public binary_function <T, T, T> {
T operator()(const T& i, const T& j) const {
return i + j;
}
};
基類binary_function<T, T, T>包含了參數和函數對象返回類型的類型定義,這樣可使得函數對象可配接.
除了上面提到的基本的函數對象外,STL還包含了binder模板,將可配接的二元函數中的某個實參固定為常量值,來創建一個一元函數對象.比如:class plus_1 {
int _i;
public:
plus_1(const int& i) : _i(i) {}
int operator()(const int& j) { return _i + j; }
};
上面的代碼顯性地創建了一個函數對象,將其參數加1.這樣的功能可用plus模板與binder模板(bind1st來等效地實現.舉例來說,下面的兩行表達式創建了一個函數對象,當它被調用時,將返回1與調用參數的和.
plus_1(1) bind1st(plus<int>(), 1)
plus<int>就是計算兩個數之和的函數對象.bind1st使被調用的函數對象的第一個參數綁定到常量1.作為上面函數對象的使用示例,下面的代碼就是將容器a中的元素加1后,輸出到標準輸出設備:
transform(a.begin(), a.end(), ostream_iterator<int>(cout),
bind1st(plus<int>(), 1));
為了使binder更加通用,STL包含了適配器(adaptors)用于函數引用與指針,以及成員函數的配接.
所有這些工具都有一個目標,就是為了能在STL算法的調用點有可能指定一個匿名的函數,換句說,就是能夠使部分代碼片斷作為參數傳給調用算法函數.但是,標準庫在這方面只做了部分工作.上面的例子說明用標準庫工具進行匿名函數的定義還是很麻煩的.復雜的函數調用表達式,適配器,函數組合符都使理解變得困難.另外,在運用標準庫這些方法時還有明顯的限束.比如,標準C++98中的binder只允許二元函數的一個參數被綁定,而沒有對3參數,4參數的綁定.這種情況在TR1實施后,引進了通用的binder后可能改善,對于使用MSVC的程序員,有興趣還可以查看下微軟針對VS2008發布的TR1增強包.
但是不管怎樣,Lambda庫提供了針對這些問題比較優雅的解決方法:
對匿名函數以直觀的語義進行創建,上面的例子可改寫成:
transform(a.begin(), a.end(), ostream_iterator<int>(cout),
1 + _1);
更直觀點:
for_each(a.begin(), a.end(), cout << (1 + _1));
絕大部分對函數參數綁定的限制被去除,在實際C++代碼中可以綁定任意的參數
分離的函數組合操作不再需要了,函數組合被隱性地支持.
Lambda表達在函數式編程語言中很常見.在不同語言中,它們的語法有著很大不同,但是lambda表達式的基本形式是:
lambda x1...xn.e
lambda表達式定義了匿名函數,并由下列的元素組成
一個簡單的lambda表達式的例子是:
(lambda x y.x+y) 2 3 = 2 + 3 = 5
在lambda表達式的C++版本中,表達式中x1...xn不需要,已預定義形式化的參數.在現在Boost.Lambda庫中,存在三個這樣的預定義的參數,叫做占位符:_1,_2,和_3.它們分別指代在lambda表達式中的第一,二,三個參數.比如,下面這樣的lambda表達式:
lambda x y.x+y
C++定義的形式將會是這樣:
_1 + _2
因此在C++中的lambda表達式沒有語義上所謂的關鍵字.占位符作為運算符使用時就隱性地意味著運算符調用是個lambda表達式.但是只有在作為運算符調用才是這樣.當Lambda表達式包含函數調用,控制結構,轉換時就需要特殊的語法調用了.更為重要的是,作為函數調用是需封裝成binder函數的形式.比如,下面這個lambda表達式:
lambda x y.foo(x,y)
不應寫成foo(_1,_2),對應的C++結構應如下:
bind(foo, _1, _2)
對于這種表達式,更傾向于作為綁定表達式bind expressions
lambda表達式定義了C++的函數對象,因此,對于函數調用的形式跟其他的函數對象一樣,比如:(_1 + _2)(i, j).
性能,運行效率,總是C++程序員關心的話題.理論上,相對于手寫循環代碼,使用STL算法和Lambda函數對象的所有運行開銷,可以通過編譯優化消除掉.這種優化取決于編譯器,實際中的編譯器大都能做到.測試表明,性能會有下降,但是影響不大,對于代碼的效率和簡潔之間的權衡,只能由程序員自己做出判斷了.
Lambda庫的設計與實現中大量運用了模板技術,造成對于同一模板需要大量的遞歸實例化.這一因素可能使構建復雜邏輯的lambda表達式,不是一個非常理想的做法.因為編譯這些表達式需要大量的內存,從而使編譯時間變得非常慢,這在一些大型項目中會更加突出.還有在發生編誤錯誤時,引發的大量錯誤信息,不能有效地指出真正錯誤之處.最后點,C++標準建議模板的嵌套層次不要超過17層來防止導致無限遞歸,而復雜的Lambda表達式模板會很容易超過這一限制.雖然大多數編譯器允許更深層次的模板嵌套,但是通常需要顯性地傳入一個命令行參數才能做到.
大多數內容是從Boost.Lambday庫在線文檔參考翻譯而成
http://doc.trolltech.com/qq/qq13-apis.html
翻譯這篇文章的目的不是讓人了解Qt,而是讓人試著學習點C++編程的軟技能。我從原文中得到的一些風格上的體會,也希望你能從中有所收獲.(譯者注)
我們在Trolltech做了大量研究來改進Qt開發體驗.在這篇文章中,我將分享我們的一些成果,呈現我們在進行Qt 4設計時所使遵循的原現,并向你展示如何將它們應用到你的代碼中.
設計應用程序接口(APIs)是有難度的.它是像跟設計編程語言一樣困難的藝術.要遵循許多不同的的原則,這些原則中的許多還彼此沖突.
現今的計算機教育過多關注于算法和數據結構,很少去關注隱藏在程序設計語言和程序框架后面的那些設計原則.這使得程序員們面對日益重要的任務,創建可復用的組件,毫無準備.
在面向對象語言出現前,通用的可復用的代碼大都由庫提供者而不是應用程序開發者來編寫.在Qt世界中,這種情況已發生了很大的變化.在用Qt編程其實就是在寫新的組件.典型的Qt應用程序都存在某些自定義的組件,在整個應用程序中被復用.相同的組件常常作為其他程序的一部分被開發出來.KDE,K桌面環境,甚至使用許多附加庫,來進一步擴展Qt,實現許多額外的類.
但是一個優秀,高效的C++ API究竟是怎樣子呢?它的好壞取決于許多因素,比如說,手頭上的任務和特定目標群體.優秀的API具有很多特性,它們的一些是普遍所要期望的,另一些是針對特定問題域的.
API對于程序員就相當于GUI對于最終用戶.API中'P'代表程序員(Programmer),而不是程序(Program),強調這一點是為了說明API是讓程序員使用的,程序員是人而不機器.
我們認為APIs應當精簡而完備,具有清晰簡單的語義,直觀,易記且應使代碼具有可讀性.
最后,請記住:不同的用戶使用API的不同部分.當簡單地使用Qt類的實例可能有直觀性,但這有可能使用戶在閱讀完有關文檔后,才能嘗試使用其中部分功能.
通常的誤讀是越少的代碼越能使你達到編寫更好的API這一目的.請記住,代碼只寫一遍,卻要一遍又一遍地去理解閱讀它.比如:
QSlider *slider = new QSlider(12, 18, 3, 13, Qt::Vertical,
0, "volume");
可以會比下面的代碼更難閱讀(甚至于編寫)
QSlider *slider = new QSlider(Qt::Vertical);
slider->setRange(12, 18);
slider->setPageStep(3);
slider->setValue(13);
slider->setObjectName("volume");
布爾參數常常導致難以閱讀的代碼.特別地,增加某個bool參數到現存的函數一般都會是個錯誤的決定.在Qt中,傳統的例子是repaint(),它帶有一個可選的布爾參數,來指定背景是否刪除(默認是刪除).這就導致了代碼會像這樣子:
widget->repaint(false);
初學者可能會按字面義理解為,"不要重繪!"
自然的想法是bool參數節省了一個函數,因此減少了代碼的臃腫.事實上,這增加了代碼的臃腫,有多少Qt用戶真正知道下面這三行代碼在做什么呢?
widget->repaint(); widget->repaint(true); widget->repaint(false);
好一點的API代碼可能看起來像這樣:
widget->repaint(); widget->repaintWithoutErasing();
在Qt 4中,我們解決這個問題的辦法是,簡單地去除掉不刪除widget而進行重繪的可能性.Qt 4對雙重緩沖的原生支持,會使這功能被廢棄掉.
這里有些例子:
widget->setSizePolicy(QSizePolicy::Fixed,
QSizePolicy::Expanding, true);
textEdit->insert("Where's Waldo?", true, true, false);
QRegExp rx("moc_*.c??", false, true);
顯然的解決辦法就是將bool 參數用枚舉類型來替換.這就是我們在Qt 4中Qstring中的大小寫敏感所做的,比較下面兩個例子:
str.replace("%USER%", user, false); // Qt 3
str.replace("%USER%", user, Qt::CaseInsensitive); // Qt 4
相似的類應該有相似的API.在某種程度上,這能用繼承來實現,也就是運用運行時多態機制.但是多態也能發生在設計時.比如,你將QListBox與QComboBox交換,QSlider與QSpinBox交換,你會發現API的相似性會使這種替換變得比較容易.這就是我們所謂的"靜態多態".
靜態多態也能使記憶API和編程模式更加容易.因而,對一組相關類的相似API有時候比為每個類設計獨特完美的API會更好.
命名有時候是設計API中最重要的事情了.某個類應叫什么名字,某個成員函數又應叫什么名字,都需要好好思考.?
有少許規則對所有類型的命名都適應.首先,正如我早先所提到的,不要用縮寫.甚至對用"prev"代表"previous"這樣明顯的縮寫也不會在長期中受益,因為用戶必須記住哪些名字是縮寫.
如果連API自身都不能保持統一,事情自然會變得更壞.比如,Qt 3中有activatePreviousWindow()函數,也有fetchPrev()函數.堅持"沒有縮寫"這條規則,會使創建一致的API更加簡單.
在設計類中,另一重要但是不明顯的規則是盡量保持子類中名字的簡潔易懂.在Qt 3中,這個原則并不總是被遵守.為了說明這一點,我們舉下QToolButton的例子.如果你在Qt 3中對QToolButton調用call name(), caption(), text(), 或 textLabel()成員函數時,你希望會發生什么?那就在Qt設計器中試試QToolButton吧.
為了可讀性的關系,在Qt4中name 被稱為objectName ,caption被稱為windowTitle,在QToolButton中為了使text明晰,不再有textLabel屬性.
不應為每個不同的類尋求完美的名字,而是將類進行分給.比如,在Qt 4中所有跟模型有關的視類的部件都用View后綴(QlistView,QTableView,QTreeView),相應的基于部件的類用Widget后綴代替(QListWidget,QTableWidget,QTreeWidge).
當設計枚舉時,我們應當記住C++中(不像Java或C#),枚舉值在使用時不帶類型名.下面的例子說明了對枚舉值取太一般化的名字的危害:
namespace Qt
{
enum Corner { TopLeft, BottomRight, ... };
enum CaseSensitivity { Insensitive, Sensitive };
...
};
tabWidget->setCornerWidget(widget, Qt::TopLeft);
str.indexOf("$(QTDIR)", Qt::Insensitive);
在上面這行中,Insensitive這個名字什么意思呢?為枚舉類型命名具有指導的原則是最好在每個枚舉值中重復枚舉類型的名字.
namespace Qt
{
enum Corner { TopLeftCorner, BottomRightCorner, ... };
enum CaseSensitivity { CaseInsensitive,
CaseSensitive };
...
};
tabWidget->setCornerWidget(widget, Qt::TopLeftCorner);
str.indexOf("$(QTDIR)", Qt::CaseInsensitive);
但枚舉值之間是一種"或"關系和被用作標志位時,傳統的解決方法是將"或"結果存為int,這樣做是類型不安全的.Qt 4提供了一模板類QFlags<T>,其中T是枚舉類型.Qt為標志類型名稱提供了便利,你能用Qt::Alignment 來代替QFlags<Qt::AlignmentFlag>.
為了方便,我們給枚舉類型單數形式的名稱(只有當只含一個標志位時),給"flags"類型復數形式的名稱,比如:
enum RectangleEdge { LeftEdge, RightEdge, ... };
typedef QFlags<RectangleEdge> RectangleEdges;
在某些情況下,"flags"類型有單數形式的名稱.在這種情況下,枚舉類型以Flag后綴標識:
enum AlignmentFlag { AlignLeft, AlignTop, ... };
typedef QFlags<AlignmentFlag> Alignment;
函數命名中的一條規則就是應能從它的名字清楚地看出函數是否著副作用.在Qt 3中,常函數QString::simplifyWhiteSpace()就違反了這規則.即然它返回QString,而不是像它的名字所表述的那樣修改字符串. 在Qt 4中,這個函數被重命名為QString::simplified().
參數名對于程序員來說是重要的信息來源,即使它們不出現在調用API的代碼中.既然現代的IDE會在程序員編碼時顯示這些參數,所以非常值得在頭文件中給這些參數取恰當的名字,在文檔中同樣使用相同的名字
給布爾型的getter,setter,屬性取個恰當的名字總是特別困難.getter應該叫checked() 或者還是叫isChecked(),取scrollBarsEnabled()還是areScrollBarEnabled()
在Qt 4中,我們對于getter的函數使用下面的指導原則
setter的命名可以從這推知,只要去掉is前綴,在名字前面加set前綴就可以了.比如setDown()和setScrollBarsEnabled().屬性的名字跟getter一樣,就是沒有is前綴
對于向外傳參,是使用指針,還是引用更好呢?
void getHsv(int *h, int *s, int *v) const void getHsv(int &h, int &s, int &v) const
絕大多數C++書籍都推薦無論何時都盡可能使用引用,因為從大多數情況來說,引用比指針有著所謂的"安全和優雅".相比而方,在Trolltech,我們更趨向于指針,因為它使用戶代碼更具可讀性.比較下面的代碼:
color.getHsv(&h, &s, &v); color.getHsv(h, s, v);
只有第一行代碼能更清楚地說明h,s,v在函數被調用后,其值極有可能被修改.
為了在實際代碼中說明這些概念,我們以QProgressBar在Qt3和Qt4中的比較進行研究.在Qt 3中:
class QProgressBar : public QWidget
{
...
public:
int totalSteps() const;
int progress() const;
const QString &progressString() const;
bool percentageVisible() const;
void setPercentageVisible(bool);
void setCenterIndicator(bool on);
bool centerIndicator() const;
void setIndicatorFollowsStyle(bool);
bool indicatorFollowsStyle() const;
public slots:
void reset();
virtual void setTotalSteps(int totalSteps);
virtual void setProgress(int progress);
void setProgress(int progress, int totalSteps);
protected:
virtual bool setIndicator(QString &progressStr,
int progress,
int totalSteps);
...
};
對這個API進行改進的關鍵之處就是需要觀察到Qt 4中QProgressBar與QAbstractSpinBox,以及它的子類,QSpinBox,QSlider,和QDial有著相似性.解決的辦法呢?將其中的progress和totalSteps用minimun,maximum和value替換.
增加valueChanged()的信號量.增加setRange()這一方便的函數.
接下來需要到progressString, percentage 和indicator實際上都指代同一東西:顯示在進度欄上的文本.通常這一文本是一百分數,但是它能被setIndicator()設置成任何值.這里是新的API:
virtual QString text() const; void setTextVisible(bool visible); bool isTextVisible() const;
默認,這文本是百分比指示器.這可以用重新實現的text()進行改變.
在Qt 3中,setCenterIndicator() 和 setIndicatorFollowsStyle()是兩個影響對齊方式的函數.它們現在都被一個高級的函數所取代,setAlignment().
void setAlignment(Qt::Alignment alignment);
如果程序員沒有調用 setAlignment(),對齊是基于的樣式決定的.對于Motif樣式,文本顯示在中間,而對于其他樣式,文本是右對齊的.
這里是改進過的QProgressBar:
class QProgressBar : public QWidget32
{
...
public:
void setMinimum(int minimum);
int minimum() const;
void setMaximum(int maximum);
int maximum() const;
void setRange(int minimum, int maximum);
int value() const;
virtual QString text() const;
void setTextVisible(bool visible);
bool isTextVisible() const;
Qt::Alignment alignment() const;
void setAlignment(Qt::Alignment alignment);
public slots:
void reset();
void setValue(int value);
signals:
void valueChanged(int value);
...
};
APIs需要質量保證.最早的版本一般都不是很好的,你必須測試它.通過調用這個API的代碼作為測試事例,來驗證代碼具有可讀性.
另外的技巧包括讓人在沒有文檔和類文檔化(類的概述和函數說明)的情況下能夠使用這個API.
當你陷入麻煩中時,文檔化也是好的辦法找出一個合適的命名:試著為這些類,函數,枚舉值標住文檔,然后使用浮現在你腦中的第一個詞匯.如果你找不到精準的名字去表述,那很有可能這個東西就不應存在.如果任何辦法都失敗了,而且你確信這個概念是有用的,那就發明一個新的名字吧.最后,不管怎么說,"widget", "event", "focus", and "buddy"這些詞總會能用上一個.
在網上可以見到某些關于Glade的教程,大都是關于Linux平臺和Glade 2的,因為原先Glade作為快速開發工具,集成代碼生成功能,生成C文件.所以常常有初學者對網上某些教程所提及的"generate"(生成代碼)功能表示迷惑,在新版本的Glade-3上找不到對應的功能.
新版本的Glade-3是對原先Glade代碼的完全重寫.一個顯著的變化就是去除了代碼生成功能.這樣做是有原因的,即然代碼生成功能不被提倡使用,而是更鼓勵使用libglade功能.但是如果你真需要代碼生成功能的話,它還是可以做為插件來提供的.另一個顯著的不同是glade-3設計用來最大化使用GObject的自省機制(GObject introspection),來使外部工具箱和部件的控制,信號和屬性的集成更加容易.
如果看過Say Hello to GTK+的話,可能感覺那樣的窗體程序太簡單了.那么現在讓我們借助Glade弄點兒復雜一點兒的界面吧.首先來瞧瞧Glade長什么樣,下圖就是Glade在windows下的界面.左邊的窗體的小部件選擇器,相當于調色板.中間是主菜單,右邊的是屬性窗體.
現在開始創建一個類似于文本編輯器的圖形界面.按照上圖標注的順序,依次添加window部件,vertical box部件,menu bar部件,text view部件和Status部件.vertical box設置三行,它是用來進行界面布局,分割空間用,這是gtk+設計與傳統的windows UI設計很不同的地方.后三個部件是放置vertical box中的,最后設計完成圖形如下.保存取名為win.glade.如果你感興趣的話,可以用文件編輯器打開這個文件看看,正如所說的那樣,它是一個xml格式的文本文件.
現在我們設置相關的頭文件和庫文件,編輯一個glade.c文件,添加進以下的代碼,運行看看,會出現如上圖的對話框.雖然這個對話框什么都不干,但是通過Glade,我們能較為容易地設計界面,而不用通過gtk函數,一個一個將控件實現.
#include <gtk/gtk.h>
#include <glade/glade.h> int main(int argc, char* argv[]) { GladeXML *gxml; GtkWidget *window; gtk_init (&argc, &argv); gxml = glade_xml_new ("win.glade", NULL, NULL); window = glade_xml_get_widget (gxml, "hello"); g_object_unref (G_OBJECT (gxml)); gtk_widget_show (window); gtk_main (); return 0; }
在Windows開發GTK+的應用程序,首先需要其在Windows版本下的庫文件,下載gladewin32項目中的gtk+-win32-devel安裝包進行安裝.我這里使用VS2005進行配置,因為相對于其他的環境,大家對于此IDE更為熟悉.接下來我將一步步介紹我在對GTK/Glande3說Hello時碰到的問題與解決方法.
任何像我一樣急切的人,都希望用最少的代碼,看一下GTK+的世界是怎么樣的.新建一"Win32 項目",然后向其添加gtk.c文件.
/* file gtk.c */
#include < gtk / gtk.h >
int main( int argc, char * argv[])
{
GtkWidget * window;
gtk_init ( & argc, & argv);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_title (GTK_WINDOW (window), " Hello World " );
gtk_widget_show (window);
gtk_main();
return 0 ;
}
現在麻煩的事情到了,設置相關的頭文件和庫文件.設置頭文件,右鍵點擊項目名,然后選擇屬性.然后在"常規"-"附加包含目錄"中設置你安裝gtk+-win32-devel時對應的頭文件目錄.然后再設置庫文件,需要gtk-win32-2.0.lib,glib-2.0.lib,gobject-2.0.lib.
設置完好后,你就可以編譯運行了,一個什么都不干的丑陋的windows窗體出現你面前了.若編譯不通過,缺少某些頭文件的話,查看出錯信息,然后用windows查找對應頭文件路徑,再設置.若連接出錯,很可能在設置庫文件出錯了,同理,設置相應的庫文件路徑.程序代碼很簡單,定義一個windows窗體指針,然后創建窗體和定義窗體標題,接著就是顯示了,和gtk主循環了.
或許你發現了,點擊窗體中的"X"關閉按鈕,雖然窗體消失了,但是黑色的命令窗口還在,發現"任務管理器"中程序還在運行著.對,它并沒有像你想的那樣,在點擊"X"關閉按鈕結束掉程序.這些是需要你告訴程序該怎么做的.我們需要在代碼中添加相應的事件處理函數. 新增加了destroy函數,它就是事件處理函數,在gtk也可叫做信號處理函數,因為gtk中用信號來進行事件的通知.destory函數的形參是有約定寫法的,這個大家可以參見具體的手冊.在主程序中,還需要將這一函數與具體的信號進行關聯,見第17行.
在完成這些工作后,你再點擊"X"按鈕時,會發現"命令行窗口"會出現"按任意鍵繼續",程序是真正地退出了.
/* file gtk.c */
#include
static void destroy( GtkWidget *widget,
gpointer data )
{
gtk_main_quit (); //退出gtk主循環
}
int main(int argc, char* argv[])
{
GtkWidget* window;
gtk_init (&argc, &argv);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_title (GTK_WINDOW (window), "Hello World");
g_signal_connect (G_OBJECT (window), "destroy", G_CALLBACK (destroy), NULL);
gtk_widget_show (window);
gtk_main();
return 0;
}
有些人會對這個gtk應用程序總會伴隨著黑黑的命令行窗口感到不爽,包括我自己在內.不知道gtk程序在gnome下也是這樣,我沒有試過,不知道.現在我就使用windows平臺的特定方法,來讓它消失吧,就是使用WinMain做為主函數.這方法或許不是最正規的做法,但我看來卻有效.如果要考慮到跨平臺的話,可以用條件編譯處理下.
/* file gtk.c */ #include#include static void destroy( GtkWidget *widget, gpointer data ) { gtk_main_quit (); //退出gtk主循環 } int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) { //為應付gtk_init所需要的參數 int argc=1; char* commandLine={"gtkApplication"}; char** argv = &commandLine; GtkWidget* window; gtk_init (&argc, &argv); window = gtk_window_new (GTK_WINDOW_TOPLEVEL); gtk_window_set_title (GTK_WINDOW (window), "Hello World"); g_signal_connect (G_OBJECT (window), "destroy", G_CALLBACK (destroy), NULL); gtk_widget_show (window); gtk_main(); return 0; }
好啦,現在再編譯運行,命令行窗口沒有了.你會發現真正的win32窗體了.但是在開發中,我建議用gtk時,還是讓命令行窗口顯示出來,因為gtk在出錯時,會把一些有用的信息打到命令行窗口中,這對你的幫助會很大.
PyGTK讓你用Python輕松創建具有圖形用戶界面的程序.底層的GTK+提供了各式的可視元素和功能,如果需要,你能開發在GNOME桌面系統運行的功能完整的軟件.
PyGTK真正具有跨平臺性,它能不加修改地,穩定運行各種操作系統之上,如Linux,Windows,MacOS等.除了簡單易用和快速的原型開發能力外,PyGTK還有一流的處理本地化語言的獨特功能.
PyGTK是自由軟件,所以你能幾乎沒有任何限制的使用,修改,分發,研究它,它是基于LGPL協議發布的.
如果你對上面提到的GTK+,也不了解的話,那允許再對它也進行一番介紹.GTK+,用C語言開發的,具有跨平臺的GUI庫,它是GNOME桌面系統(如果你在用Linux,一定不陌生)和GIMP圖象編輯器的開發工具箱.它是世界上許多程序員的選擇,對他們來說,國際化的支持是必要的,而且性能也總是他們考慮的因素.與GTK同一領域的還有Qt庫,它是由商業公司開發的C++圖形庫,雖然它也有免費的.
在windows平臺的安裝和開發
安裝PyGTK只需執行下列步驟:
或許你對這些步驟還感到麻煩,或者對Python不熟悉的話,那也沒有關系,直接下載一鍵安裝包all-in-one installer,為你配置好全部運行時環境.
看看開發環境是否配置正確,將下列代碼作為Python腳本或者在Python交互控制臺下輸入.如果正確的話,應該有一個標題為"Hello World"的windows的空窗口呈現在你面前.
如果不能運行的話,有可能會出現一個不能成功加載dll的錯誤提示,這是因為缺少iconv.dll.這時需要只需從網上下載過來,拷貝至windows/system32目錄下即可了.
import gtk
window = gtk.Window()
window.set_title("Hello World")
window.show_all()
gtk.main()
FlexeLint for C/C++是在PC_lint在windows平臺獲得成功后,同樣由Gimpel公司開發的,以源代碼形式發布的,在Unix/Linux平臺上的靜態代碼分析工具。
本文主要介紹PC-lint的安裝與配置,因此是在windows平臺上進行討論。
PC-lint支持幾乎所有流行的編譯器和IDE環境,可能因為其發展歷史和面對專業程序員群體的原因,它是以命令行加配置文件的形式進行使用,所以其使用習慣跟現在常見的windows軟件不同。
現以PC-lint與VS2005進行集成來說明:
將PC-lint釋放到某一目錄下,如:D:\Program Files\pclint.將新建一std.lnt文件在主目錄中,并將添加上以下的內容
au-sm.lnt co-msc80.lnt lib-mfc.lnt lib-stl.lnt lib-w32.lnt lib-wnt.lnt lib-atl.lnt options.lnt -si4 -sp4 -i "C:\Program Files\Microsoft Visual Studio 8\VC\include" -i "C:\Program Files\Microsoft Visual Studio 8\VC\atlmfc\include" -i "C:\Program Files\Microsoft Visual Studio 8\VC\PlatformSDK\include" -i "C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\include"
注意:-i后面為相應的vc頭文件目錄路徑,而一系列xxx.lnt是語法配置規則,決定了按什么規則進行檢查,以后可以根據需要進行增減. 寫在配置文件中相應的的xxx.lnt從lnt子目錄中,拷貝到主目錄中.(這一步很重要) 在vs2005中的工具->外部工具中,點擊"添加",新建一個外部工具.標題可以任意,可取(pc_lint);命令為:D:\Program Files\pclint\LINT-NT.EXE;參數為:-i"D:\Program Files\pclint" std.lnt "$(ItemFileName)$(ItemExt)";初始目錄為:$(ItemDir),并將下面的"使用輸出窗口"勾選上. 接下來,就可以寫一段程序,在工具菜單中選擇pc_lint 來進行檢查了.如果你編寫的程序有不符合定義的規范,則會在輸出窗口中出現相關的信息.