乐者为王

Do one thing, and do it well.

Hello wxWidgets!

MainApp.h代码:

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef __MAINAPP_H__
#define __MAINAPP_H__

#include <wx/wx.h>

class MainApp : public wxApp
{
public:
    virtual bool OnInit();
};

#endif

MainApp.cpp代码:

1
2
3
4
5
6
7
8
9
10
11
12
#include "MainApp.h"
#include "MainFrame.h"

IMPLEMENT_APP(MainApp)

bool MainApp::OnInit()
{
    MainFrame* frame = new MainFrame("Hello wxWidgets!");
    SetTopWindow(frame);
    frame->Show();
    return true;
}

MainFrame.h代码:

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef __MAINFRAME_H__
#define __MAINFRAME_H__

#include <wx/frame.h>

class MainFrame : public wxFrame
{
public:
    MainFrame(const wxString& title);
};

#endif

MainFrame.cpp代码:

1
2
3
4
5
6
#include "MainFrame.h"

MainFrame::MainFrame(const wxString& title) : wxFrame(NULL, wxID_ANY, title)
{
    // TODO: add member initialization code here
}

PowerBuilder中如何调用默认的邮件客户端

在某个窗口上有一个文字超链接(StaticHyperLink控件),点击后就打开默认的邮件客户端,并且把指定收件人的邮件地址填写到收件人地址栏中。

如果在StaticHyperLink控件的URL中使用mailto协议,的确能够做到上述的要求。但是有个问题是在打开邮件客户端前总是先打开一个IE窗口,这样使得用户的体验非常差。而PowerBuilder自带的mailsession对象则只能调用Outlook Express,根本就不用考虑。

查找资料后发现可以通过调用外部函数ShellExecute来解决这个问题。首先在Global/Local External Functions中添加如下声明:

1
2
3
4
5
6
function ulong ShellExecuteA(ulong hwnd, &
                            string lpOperation, &
                            string lpFile, &
                            string lpParameters, &
                            string lpDirectory, &
                            long nShowCmd) library "shell32"

然后在StaticHyperLink控件的Clicked事件中添加代码:

1
2
3
4
5
6
7
8
9
string ls_null

SetNull(ls_null)
ShellExecuteA(Handle(parent), &
            "open", &
            "mailto:yourname@example.com", &
            ls_null, &
            ls_null, &
            1)    // SW_SHOWNORMAL

还有要注意4个string类型的参数,一定是string xxx,千万不要使用下面这种声明方式:

1
2
3
4
5
6
function ulong ShellExecuteA(ulong hwnd, &
                             ref string lpOperation, &
                             ref string lpFile, &
                             ref string lpParameters, &
                             ref string lpDirectory, &
                             long nShowCmd) library "shell32"

刚开始的时候我就使用了这种声明方式,结果每次程序执行到ShellExecuteA时都会弹出一个异常对话框,搞的我非常郁闷。

身份证工具IDCardTool

周末赶了两天,终于把这个工具完成了。界面几乎完全仿制了“身份证生成与查询BBSt(共享版)”。

ict-main

中国居民身份证的常识

我国现行使用公民身份证号码有两种尊循两个国家标准,〖GB 11643-1989〗和〖GB 11643-1999〗。

〖GB 11643-1989〗中规定的是15位身份证号码:排列顺序从左至右依次为:六位数字地址码,六位数字出生日期码,三位数字顺序码,其中出生日期码不包含世纪数。

ict-15

〖GB 11643-1999〗中规定的是18位身份证号码:排列顺序从左至右依次为:六位数字地址码,八位数字出生日期码,三位数字顺序码,一位数字校验码。

ict-18

  1. 地址码:表示编码对象常住户口所在县(市、旗、区)的行政区划代码
  2. 出生日期码:表示编码对象出生的年、月、日,年、月、日之间不用分隔符。
  3. 顺序码:表示同一地址码所标识的区域范围内,对同年、同月、同日出生的人员编定的顺序号。顺序码的奇数分给男性,偶数分给女性。
  4. 校验码:根据前面十七位数字码,按照国际标准ISO 7064:1983.MOD 11-2规定的校验算法计算出来的检验码。

关于身份证号码最后一位校验码的算法如下:

1
∑(a[i] * W[i]) mod 11 (i = 2, 3, ..., 18)
  • *:表示乘号;
  • i:表示身份证号码每一位字符的位置序号,从右至左,最左侧为18,最右侧为1;
  • a[i]:表示身份证号码第i位上的号码数字值;
  • W[i]:表示第i位上的加权因子,其数值依据公式W[i] = 2i-1 mod 11计算得出。

设:R = ∑(a[i] * W[i]) mod 11 (i = 2, 3, ..., 18),C = 身份证号码的校验码,则R和C之间的对应关系如下表:

ict-check

由此看出Ⅹ相当于10(罗马数字中的10就是用Ⅹ表示),所以在新标准的身份证号码中可能含有非数字的字母Ⅹ。

代码下载:https://github.com/dohkoos/IDCardTool

数据窗口中高亮一行时出现的问题

高亮数据窗口中的一行很简单,只要在数据窗口控件的rowfocuschanged事件中添加下面的代码就可以了:

1
2
3
4
5
long ll_row = this.GetRow()
if ll_row > 0 then
    this.SelectRow(0, false)
    this.SelectRow(ll_row, true)
end if

不过这样处理后会有些问题,当你点击某一行时该行会显示成边框为深蓝色,背景为白色的矩形,如下图:

pb-highlight-1

这是因为该行中列的样式类型是Edit,可以编辑,所以才会出现上图的效果。由此推断,只要将列设置为不可编辑就行。将一列设为不可编辑有三种方式:TabSequence为0、Edit.DisplayOnly被选中、Protect为1,设置其中任何一种都会使列变为不可编辑。

实践后发现选中Edit.DisplayOnly后还是会出现如上图那样的问题,而在设置TabSequence或Protect后则显示出了完美的高亮效果。

pb-highlight-2

Visual Studio 2005下wxWidgets-2.8.6开发环境的搭建

wxWidgets是一个开源的跨平台C++类库,它包含一个可以支持现今几乎所有操作系统的图形用户界面(GUI)库和其它一些很有用的工具,提供了类似MFC的功能。wxWidgets的主体是由C++构建的,但你并不是必需通过C++才能使用wxWidgets。wxWidgets拥有许多其它语言的绑定,使你在用其它语言编写程序的时候也可以使用wxWidgets。

wxWidgets的下载、安装和编译

  1. http://www.wxwidgets.org/downloads/ 下载一个wxMSW版本的wxWidgets;
  2. 运行安装文件。在安装好后设置环境变量WXWIN,指向wxWidgets的安装目录。因为安装过程仅仅是把文件拷贝到指定的目录,所以还需要对wxWidgets进行编译;
  3. 进入$(WXWIN)/build/msw目录,用VS2005打开wx.dsw,提示是否进行项目转换,点确定。如果想要链接静态运行库,在编译前应该对每个项目进行设置,如下图:

wxwidgets-compile

编译好后的wxWidgets会在$(WXWIN)/lib/vc_lib目录下生成一系列的lib文件,这些lib文件的名字遵循下面的命名规则:不依赖于GUI组件的库会以wxbase开头,紧跟着的是版本号,然后的字母表明这个库是否是编译为Unicode('u')或是否是编译为Debug('d'),名字中的最后部分是wxWidgets组件的名字。

注意:wxWidgets编译完后在$(WXWIN)/build/msw目录下会生成许多预编译头文件,占了很大的空间,如果确定以后不再编译wxWidgets库的话,可以考虑删掉。

设置开发环境

打开Tools -> Options -> Projects and Solutions -> VC++ Directories

  1. 在Include files中加入$(WXWIN)/include和$(WXWIN)/include/msvc
  2. 在Library files中加入$(WXWIN)/lib/vc_lib

创建wxWidgets项目

新建一个General -> Empty Project或Win32 -> Win32 Project项目,然后设置项目的一些属性,可以选择下面的其中一个来设置。譬如你如果打算使用Unicode,那么选择Unicode Debug或Unicode Release,如果你仅仅想调试程序而非发布,则只需要选择Debug设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Debug:
Project Properties -> General -> Character Set: No Set
Project Properties -> C/C++ -> Code Generation -> Runtime Library: Multi-threaded Debug (/MTd)
Project Properties -> Linker -> Input -> Additional Dependencies: wxbase28d.lib wxmsw28d_core.lib winmm.lib comctl32.lib rpcrt4.lib wsock32.lib oleacc.lib

Release:
Project Properties -> General -> Character Set: No Set
Project Properties -> C/C++ -> Code Generation -> Runtime Library: Multi-threaded (/MT)
Project Properties -> Linker -> Input -> Additional Dependencies: wxbase28.lib wxmsw28_core.lib winmm.lib comctl32.lib rpcrt4.lib wsock32.lib oleacc.lib

Unicode Debug:
Project Properties -> General -> Character Set: Use Unicode Character Set
Project Properties -> C/C++ -> Code Generation -> Runtime Library: Multi-threaded Debug (/MTd)
Project Properties -> Linker -> Input -> Additional Dependencies: wxbase28ud.lib wxmsw28ud_core.lib winmm.lib comctl32.lib rpcrt4.lib wsock32.lib oleacc.lib

Unicode Release:
Project Properties -> General -> Character Set: Use Unicode Character Set
Project Properties -> C/C++ -> Code Generation -> Runtime Library: Multi-threaded (/MT)
Project Properties -> Linker -> Input -> Additional Dependencies: wxbase28u.lib wxmsw28u_core.lib winmm.lib comctl32.lib rpcrt4.lib wsock32.lib oleacc.lib

问题

1、编译时出现

1
Cannot open include file: '../mswu/wx/setup.h': No such file or directory

是因为项目属性的Character Set设置不正确,必须与使用的wxWidgets库的Character Set一致。

2、编译时如果出现

1
MSVCRT.lib(MSVCRT.dll) : error LNK2005: _free already defined in LIBC.lib(free.obj)

这是因为没有连接正确的wxWidgets库,譬如Unicode Debug版本的项目就需要连接Unicode+Debug版本的wxWidgets库(库名后缀为'ud'的lib文件),或者是wxWidgets库和程序使用的Runtime Library不同。

3、运行时出现No Debugging Information对话框。

将Project Properties -> Linker -> Debugging -> Generate Debug Info的值改成Yes (/DEBUG)。

数据窗口中Column控件的EditMask.UseFormat属性的作用

当数据窗口中的EditMask控件失去焦点时,如果EditMask.UseFormat为yes,则使用Format属性值格式化EditMask的内容,否则,使用EditMask.Mask属性值格式化控件的内容。当EditMask得到焦点时,使用EditMask.Mask格式化内容。

根据此规则写的伪代码如下:

1
2
3
4
5
6
7
8
9
if (isFocused) {
    EditMask.Mask
} else if (loseFocus) {
    if (EditMask.UseFormat == "yes" && Format != "") {
        Format
    } else {
        EditMask.Mask
    }
}

Painter: Use Format option

使用ANTLR构建PowerScript语法分析器(3)

转义字符(Special ASCII Characters)

PowerScript中转义字符是以波浪号(~)开头。下图是PowerScript支持的完整的转义字符,#字符表示数字。Decimal由三个十进制数组成,范围是000-255;Hexadecimal由两个十六进制数组成,范围是00-FF;Octal由三个八进制数组成,范围是000-377。

pb-special-char

注意:#表示数字是必须的。譬如,必须用~007这种样式来表示转义字符,而不能是~7这种。

这里是转义字符的词法规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fragment
Escape
    : '~' ('n'|'t'|'v'|'r'|'f'|'b'|'\"'|'\''|'~')
    | DecimalEscape
    | HexEscape
    | OctalEscape
    ;

fragment
DecimalEscape
    : '~' ('0'..'1') ('0'..'9') ('0'..'9')    // 000 - 199
    | '~' '2' ('0'..'5') ('0'..'5')           // 200 - 255
    ;

fragment
HexEscape
    : '~h' ('0'..'9'|'a'..'f'|'A'..'F') ('0'..'9'|'a'..'f'|'A'..'F')    // 00 - FF
    ;

fragment
OctalEscape
    : '~o' ('0'..'3') ('0'..'7') ('0'..'7')   // 000 - 377
    ;

字符和字符串字面量(Character and String Literals)

PowerScript程序中字符和字符串字面量没有明显的区别。字符是指由单引号(')或双引号(")括起来的一个ASCII字符,例如:

1
2
3
char c
c = 'T'
c = "T"

字符串字面量则是指由单引号或双引号括起来的不多于1024个的ASCII字符串,例如:

1
2
3
string s
s = 'This is a string'
s = "This is a string"

由于两者没有明显区别,因此在词法分析时只能将两者都作为字符串字面量来处理。至于到底是字符还是字符串,只有到了语意分析阶段才能作出判断。

1
2
3
4
5
6
7
8
STRING_LITERAL
    : ( '\'' (Escape|~('~'|'\r'|'\n'|'\''))* '\''
      | '\"' (Escape|~('~'|'\r'|'\n'|'\"'))* '\"'
      )
      {
          System.out.println("string>" + getText());
      }
    ;

数字和布尔字面量(Numeric and Boolean Literals)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
INTEGER_LITERAL
    : ('0'|'1'..'9' ('0'..'9')*)
      {
          System.out.println("integer>" + getText());
      }
    ;

FLOAT_LITERAL
    : ( ('0'..'9')+ '.' ('0'..'9')* Exponent?
      | '.' ('0'..'9')+ Exponent?
      | ('0'..'9')+ Exponent?
      )
      {
          System.out.println("float>" + getText());
      }
    ;

BOOLEAN_LITERAL
    : ('true'|'false')
      {
          System.out.println("boolean>" + getText());
      }

    ;

fragment
Exponent
    : ('e'|'E') ('+'|'-')? ('0'..'9')+
    ;

标识符(Identifier)

标识符规则可以参看PowerScript标识符一文,以下是标识符的词法规则:

1
2
3
4
5
6
7
8
9
10
11
IDENTIFIER
    : Letter (Letter|'0'..'9'|'$'|'#'|'%')*    // 暂时不支持短横线(-)
      {
          System.out.println("identifier>" + getText());
      }
    ;

fragment
Letter
    : ('A'..'Z'|'a'..'z'|'_')
    ;

Ruby DBI的安装和使用

Ruby DBI是一个为Ruby程序访问数据库提供的与数据库无关的统一数据库编程接口。结构层次上可以分为两层:

  1. Database Interface 数据库接口层,与数据库无关,提供与数据库无关的标准接口
  2. Database Driver 数据库驱动,与数据库相关

Ruby DBI模块可以从 http://ruby-dbi.sourceforge.net/ 取得,下载后解压缩,配置,然后安装:

1
2
3
ruby setup.rb config --with=dbi,dbd_mysql
ruby setup.rb setup
ruby setup.rb install

注意:--with参数必须根据你所安装的数据库类型来选择。可以配置多个,但不能选择没有安装在你机器上的数据库类型。记得刚开始的时候我使用ruby setup.rb config来配置(我机器上只安装了MySQL和SQL Server),结果在ruby setup.rb setup时一直出现像下面这样的错误信息:

ruby-dbi

下面是一个完整的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# dbitest.rb
require 'dbi'

# Connect to a database
dbh = DBI.connect('dbi:Mysql:rubydbi:localhost', 'root', '123456')

dbh.do('DROP TABLE IF EXISTS people')
dbh.do('CREATE TABLE people (id int, name varchar(30))')

# Insert some rows, use placeholders
sql = 'INSERT INTO people VALUES (?, ?)'
dbh.prepare(sql) do |sth|
    sth.execute('100', 'Michael')
    sth.execute('200', 'John')
end

# Select all rows
sth = dbh.prepare('SELECT * FROM people')
sth.execute

# Print out each row
while row = sth.fetch do
    p row
end

# Close the statement handle when done
sth.finish

dbh.do('DROP TABLE people')

# Finally, disconnect
dbh.disconnect

使用ANTLR构建PowerScript语法分析器(2)

使用ANTLR构建PowerScript语法分析器(1)中写的词法规则存在几个小问题:

  1. 换行符和回车符同时存在于WS和EndOfLine两个词法规则中,属于重复定义;
  2. 输出单行注释时紧随其后会额外多输出一个空行;
  3. 单行注释是文件的最后一行时不能被词法分析器识别;
  4. /* comments /* nested comments */ */这种嵌套注释会被输出成:
1
2
bc>/* comments /* nested comments */
bc>/* comments /* nested comments */ */

下面我们将逐个修复它们。

解决第1个问题前先要问问自己,单行注释的词法规则中真的需要包含EndOfLine这个词法规则吗?真的需要吗?如果没有的话,当词法分析器识别到换行符或回车符时,就会去匹配词法规则WS。这样看来,EndOfLine这个词法规则在这里其实是不必要的,完全可以删除掉。

第2个问题因为词法规则EndOfLine已经被删除,所以也就不存在了。这儿仅仅分析下问题的原因。词法分析器分析单行注释时,EndOfLine会被当作单行注释的一部分予以识别。翻看生成的词法识别器代码,可以看到打印代码块出现在词法规则EndOfLine调用的后面。打印代码块中的getText()方法是从CharStream中获取文本的,如果在词法规则EndOfLine被调用后才执行,因为词法规则EndOfLine被调用时会将CharStream的当前索引向尾部移动,这时获得的文本就会包含EndOfLine,导致打印时额外输出一个空行。

第3个问题同样也由于第1个问题的修复而不存在了。它出现的原因是因为文件最后一行的结尾字符是EOF,无法匹配词法规则EndOfLine。

第4个问题则可以通过计数器来处理。先初始化一个计数器变量depthOfComments来标记块注释的深度,当遇到“/*”时就加1,遇到“*/”时则减1。输出时只要判断depthOfComments是否等于0就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@lexer::members {
    int depthOfComments = 0;
}

BLOCK_COMMENT
    : '/*' {depthOfComments++;}
      ( options {greedy=false;}
      : ('/' '*')=> BLOCK_COMMENT
      | '/' ~('*')
      | ~('/')
      )*
      '*/' {depthOfComments--;}
      {
          if (depthOfComments == 0) {
              System.out.println("bc>" + getText());
          }
      }
    ;

使用ANTLR构建PowerScript语法分析器(1)

首先,让我们从最简单的做起,先实现一套能正确分析PowerScript注释的词法规则。PowerScript支持两种注释形式:单行注释(//)和多行注释(/* ... */)。

单行注释以双斜杠(//)起头,直到遇到行结束符为止,所以在这里要先明确一下行结束符用什么表示。在DOS系统中行结束符用“\r\n”表示,在Unix系统中行结束符用“\n”表示,现在的Mac系统也是以“\n”表示行结束符,但早期的Mac系统中行结束符则是以“\r”表示。所以兼容这几种操作系统的行结束符词法规则为:

1
2
3
4
5
6
fragment
EndOfLine
    : '\r' '\n'    // DOS
    | '\r'         // Mac
    | '\n'         // Unix
    ;

现在,有了行结束符词法规则,就可以定义单行注释的词法规则了:

1
2
3
LINE_COMMENT
    : '//' ~('\n'|'\r')* EndOfLine
    ;

通常的多行注释词法规则是这样的:

1
2
3
BLOCK_COMMENT
    : '/*' ( options {greedy=false;} : . )* '*/'
    ;

但由于PowerScript支持嵌套注释,所以上面的规则无效,必须重新定义。当词法分析器遇到“/*”后就进入了多行注释。这时假如遇到了“/”符号,词法分析器必须预取一个字符,看这个字符是不是“*”。如果是的话,就进入了嵌套的多行注释,否则,则还是处在多行注释中。

1
2
3
4
5
6
7
8
9
BLOCK_COMMENT
    : '/*'
      ( options {greedy=false;}
      : ('/' '*')=> BLOCK_COMMENT    // 在多行注释中遇到“/*”,用语法断言确定是嵌套多行注释
      | '/' ~('*')                   // 在多行注释中只遇到“/”,而没有紧跟着的“*”
      | ~('/')                       // 在多行注释中没有遇到“/”
      )*
      '*/'
    ;

当然,我们还希望忽略空格、制表符、回车符、换行符等无意义字符。

1
2
3
WS
    : (' '|'\t'|'\r'|'\n') {$channel=HIDDEN;}
    ;

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* This is a comment.   */
/* /This is a comment. */
/* *This is a comment. */
/* This is a comment. /* */*/
/* This is a comment. /* This is a nested comment. */ */
/*  The comment starts here.
 /*  The nested comment starts here.
 The nested comment ends here.  */
 The comment ends here.  */
/* The comment starts here,
 continues to this line,&
 and finally ends here. */
// This entire line is a comment.
// This entire line is a comment. //
// /This entire line is a comment.
// This entire line is a comment. */
// This entire line is a comment. /*

完整的PowerScript.g的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
grammar PowerScript;

program
    : LINE_COMMENT+
    | BLOCK_COMMENT+
    ;

LINE_COMMENT
    : '//' ~('\n'|'\r')* EndOfLine
      {
          System.out.println("lc>" + getText());
      }
    ;

BLOCK_COMMENT
    : '/*'
      ( options {greedy=false;}
      : ('/' '*')=> BLOCK_COMMENT
      | '/' ~('*')
      | ~('/')
      )*
      '*/'
      {
          System.out.println("bc>" + getText());
      }
    ;

WS
    : (' '|'\t'|'\r'|'\n') {$channel=HIDDEN;}
    ;

fragment
EndOfLine
    : '\r' '\n'    // DOS
    | '\r'         // Mac
    | '\n'         // Unix
    ;