更多感受-Hello Database
作者:南郁(nanyu)
www.d2school.com
学习本节课程,要求您已经通过预备课程安装好Firebird。请参考:《Firebird
数据库安装》。
程序就是处理数据的过程。而数据,无论处理之前或或处理之后的,往往都需要保存在程序文件之外。通常有两种形式:一种是普通的文件。比如你用Word写了一篇文章,写完后可以保存成一个Word文档。另一种,就是存入数据库。当然,数据库也必然要以某种形式存放在磁盘上;但当我们谈“数据库”时,往往是指一个完整的“数据库系统”。除了包括用于存储数据的文件,“数据库系统”本身还包括提供一些服务程序,用来管理数据。
相比普通的文件系统,数据库系统的最基础的作用体现在以下几点:
-
海量数据的管理。包括进行合适的内容组织方式以支持快速存储、查找。这里“数据”不仅仅指实质的数据,还包括数据之间的逻辑关系。
- 数据访问的安全机制,包括提供用户权限机制。
- 数据并发访问支持。
不过,我们今天的Hello
Database仅仅需要完成简单的“感受”。我们将访问一个现成的数据库中的一张数据表。表名为:“GoldenGirls12”——表里面
存有12位绝色美女的“相片”。我们的主要目标就是从数据库中读出相片,并显示在界面即可。在这一过程中,我们会“感受”到:
- 如何连接一个数据库。
- 如何通过SQL语句来查询指定表中的记录。
- 如何处理类似相片这样的二进制数据字段。
- 由于照片是JPEG格式,所以我们还将附加学习到如何将其通过“内存流”直接转换为位图格式,以便TImage控件显示。
另外,课程中有些内容不得不区分为“Turbo C++”和“C++ Builder 6”两个版本(为了不和10年的Turbo
C引起误会,有时我也会把Turbo C++ 称为C++ Builder 10.0)。
可以有很多种方法访问数据库,比如相对古老的BDE或ODBC,也可以是通过ADO,还有10.0版本之后才支持dbExpress。如果后台数据库是Interabase或Firebird,那么还可以选择IBX控件组来访问。
本来,统一采用IBX,就可以不区分TC和CB了。可是,IBX属于第3方控件(尽管CB安装后就自带);而我们用的TC是免费版本,它不允许我们使用第3方控件。所以在Turbo
C++里,我们采用dbExpress。而在C++ Builder里我们使用IBX。以后我们还会学习更多的方法。
第4小节:Hello Database!
4.1 理解分层设计
开始之前,我们先通过一张用于示意图,它展示了数据库应用程序在设计过程中,所应有的层次。

(数据库应用的分层设计)
这里仅画出最主要的3层:“客户端界面/GUI”、“中间层”、“数据库系统”。对设计过程,每个层次都可以继续划分下去;而对于物理部署,以上三层往往位于不同的机器上。
我们在预备课程里安装的Firebird,就是对应其中的“数据库系统”。对一套多用户的数据库系统,将“数据库系统”单独安装在一台电脑上,其必要性不言而喻:存在多个用户要从多台电脑上的客户端访问“数据库”的情况——这是典型的“网络数据库应用”(或多层数据库应用)。
但对于小系统,当然也可以全部安装
在同一台电脑上。比如我们今天的例子时在,数据库系统和我们的应用程序客户端安装在同一台机器上,而“中间层”和“客户端”的区分仅仅体现程序设计中的模块划分——这样的小系统通常也被称呼为“桌面数据库应用”,有点类似于ACCESS数据库。但记住,真实的情况是:我们对桌面数据库应用完全不感兴趣,我们写的例子事实上还是一个“网络数据库应用”,如果您有两台联网的机器,那么将数据库系统安装在单独的一台机器上,立即就是一个网络数据库应用。
在后面的课程中,大家不仅要完成课程的代码,还必须于代码中体会上图中的分层如何在体现。
用CB6新一个“图形用户界面”工程,或者用TC新建“VCL
Form Application”。
Step1 :设计Form1(主窗口)以下属性:
| 属性 |
值 |
说明 |
| Caption |
金陵十二钗 |
|
| Font |
宋体、CHINESE_GB2312、五号 |
在字体选择框内设置 |
| Position |
poDesktopCenter |
用于让窗口自动显示在屏幕中间 |
| Width |
800 |
宽度 |
| Height |
482 |
高度 |
| BorderStyle |
bsDialog |
显示为对话框(不能最大化) |
Step2 :在Form1左上角放一个TImage控件(在 Addtions控件页),并设置它的以下属性:
| 属性 |
值 |
说明 |
| Left |
1 |
|
| Top |
1 |
|
| Width |
600 |
|
| Height |
450 |
|
| AutoSize |
True |
自动显示为图片实际大小 |
Step3 :在Form1右上角放一个TMemo控件(在Standard控件页),并设置它的以下属性:
| 属性 |
值 |
说明 |
| Left |
605 |
|
| Top |
1 |
|
| Width |
185 |
|
| Height |
200 |
|
| ReadOnly |
True |
只读(用户不能修改其内容) |
Step4:在From1右下角,再放一个TListBox控件(在Standard控件页),并设置它的以下属性:
| 属性 |
值 |
说明 |
| Left |
605 |
|
| Top |
204 |
|
| Width |
185 |
|
| Height |
202 |
|
Step5:最后,在ListBox1下面剩余的那点空间,放一个我们很熟悉的TButton(在Standard控件页)。Caption设置为为“进入大观园”——噢,你应该知道我说的12位美女是谁了吧?
设计结果如下 ,不过因为图片太大,我大约将它缩小为60%:

(Hello Database 界面设置结果)
保存工程。将本单元的CPP文件保存为“main.cpp”。
点击此处下载本课程示例数据库文件,解压后,将它保存硬盘上某个目录内。
不要选择存放在根目录。另外,有些人图简单,喜欢将下载的文件都“扔”在桌面上,然后不去整理。这应当被看成是一种“恶习” :( 。
网上还有人说Firebird的数据库文件如果存入在带汉字的目录内,会有问题,不过我似乎没有遇上过,这里仅做提醒。
下面的例子,都假设完整的例子数据库文件路径为:“E:\白话CPP练习\DBDemo\DEMO_DB.FDB”。在做练习时,请替换为您所保存的目录。 4.4 数据模块设计
有了数据库文件之后,如何访问该文件呢?这相当于中间层的一部分。下面我们区分TC和CB进行说明。如果您有时间将两个版本的设计进行对比,会发现其实二者的区别很小。
(转到 C++
Builder的版本)
4.4.1 准备数据库连接
Turbo C++ 直接在IDE中集成了建立数据库连接的功能。在工程管理窗口(Project Manager,热键:Ctrl +
Atl + F11),切找到“数据导航页/Data Explorer”。展开“dbExpress”节点,然后选择“INTERBASE”项,右键点击出现菜单:

(通过 Data Explorer建立新的数据库连接)
点击如图的“Add New Connection”菜单项,出现对话框:

(设置要添加的数据库连接类型和连接名字)
确认之后,在Data
Explorer的树形结构中,将会增加一个名为“D2SCHOOL_DEMO”的项目,接下来是修改这个新增的数据库连接:

(准备修改新增的数据库连接)
出现对话框内,做如下设置:

(设置数据库连接)
设置Database的值为刚才下载的DEMO_DB.FDB的绝对路径(见4.3小节),也就是完整的路径。
设置HostName为:localhost,或者:127.0.0.1。
| HostName 一项用于设定数据库文件(如例中的DEMO_DB.FDB)所在主机名,也可以是主机的IP地址。
本例中,数据库就放在本机。对于本机,有个固定的主机名:localhost表示,或者也可以填写IP地址,固定为:127.0.0.1。
另外,该对话框中,我们还需注意到,Firebird/Interbase数据库的默认用户名(UserName)为sysdba;而默认密码为masterkey。 |
完成上述两项置后,我们可以测试一下是否能正确连接上数据库,还是在刚才那个对话框中,按下方的Test按钮,如果出现“Connection Successfull”,就是成功了。否则,前面那两项设置搞错了。
4.4.2 创建数据模块
选择Turbo C++的主菜单:File->New->Others。 出现的对话框中,左边树形结构中选择“C++ Builder
Files”;最后在右边的Items中选择“Data Module”。点确认退出。
IDE自动我们创建一个Data
Module,你会发现它和一个普通窗口似乎有些区别,比如它没有标题——事实上,在运行期时,并不存这一个窗口,IDE这样做,仅仅是为了方便我们往它上面添加一些数据库访问控件,也就是前面所谈的“中间层”。
检查一下,Data Module的Name属性应为默认的DataModule1,如果不是,将它改成是。
按Ctrls
+ S,将新建的文件保存为:dm.cpp。 在dbExpress控件页,找到控件“TSQLConnection”,加到Data
Module中。
 然后,再找到控件“TSQLQuery”,同样添加到Data
Module中。结果如下图:

选中“SQLConnection1”,然后设置其ConnectionName属性为“D2SCHOOL_DEMO”,也就是我们在上一步建立的数据库连接名。
 然后选中SQLQuery1,设置其SQLConnection属性为“SQLConnection1”。
 4.4.3 登录数据库
切换到4.2步骤中的主界面单元,并切到其CPP文件(本例中保存为main.cpp)。在该源文件中比较靠前的位置,找到#include
"main.h" 这一行前加入一行: #include "dm.h"
注意引号为半角英文字符。 再切换到主界面,双击“进入大观园”按钮,IDE切换main.cpp中,并且自动添加按钮OnClick的事件,输入一行代码,用于连接到我们的数据库。
//---------------------------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
DataModule1->SQLConnection1->Connected = true;
}
//---------------------------------------------------------------------------
|
保存全部。然后编译,运行。点击“进入大观园”……出现一个对话框?前面提到过Firebird/Interbase数据库的默认密码,没印象?肯定是你没认真细读课程了,回头找找吧。输入用户名和密码,我们来到大观园啦……可是,好像什么反应也没有啊?
下面的代码就是如何请出金陵十二钗了…… 4.4.4 列出名字
继续前面Button1Click的函数代码:
//---------------------------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
DataModule1->SQLConnection1->Connected = true;
DataModule1->SQLQuery1->SQL->Text = "SELECT NAME FROM GoldenGirls12";
DataModule1->SQLQuery1->Open();
while (!DataModule1->SQLQuery1->Eof)
{
AnsiString girlName = DataModule1->SQLQuery1->FieldByName("NAME")->AsString;
this->ListBox1->Items->Add(girlName);
DataModule1->SQLQuery1->Next();
}
DataModule1->SQLQuery1->Close();
}
//---------------------------------------------------------------------------
|
代码中,除了前面那句用于将SQLConnection1的连接属性打开以外,后面的代码做什么呢?我们这简单解释一下。
首先是设置SQLQuery1的SQL内容:SQL->Text = ...; 然后打开该查询:SQLQuery1->Open();
就这样两句,我们现在已经查询数据库里的十二位古典美女的名字,接下来就是如何在列表框中列出这12个名字。代码中有一个循环语句,依次读出每条记录的“NAME”字段的值,并加入ListBox1中。
最后一行代码用于关闭查询结果。 保存成果。运行,您知道金陵十二钗最后一位是谁吗? 4.4.5 显示介绍
我们的目标是:点我们选中列表框中的某一名字时,左边的Image1控件可以显示该美女的图片,而右上角的Memo1显示介绍其人的诗。显示诗比较简单一些,先来解决它。
和按钮/Button类似,列表框/ListBox也有OnClick事件。当用户选中不同行时,这一事件就会发生。在Form上选中ListBox1,双击,切换到事件代码,编辑代码:
//---------------------------------------------------------------------------
void __fastcall TForm1::ListBox1Click(TObject *Sender)
{
int selectedItem = this->ListBox1->ItemIndex;
if (-1 == selectedItem)
{
return;
}
AnsiString girlName = this->ListBox1->Items->Strings[selectedItem];
DataModule1->SQLQuery1->SQL->Text = AnsiString("SELECT * FROM GoldenGirls12 WHERE NAME = ")
+ "'" + girlName + "'";
DataModule1->SQLQuery1->Open();
if (DataModule1->SQLQuery1->Eof) //没查着?不会吧??
{
return;
}
this->Memo1->Text = DataModule1->SQLQuery1->FieldByName("POEM")->AsString;
DataModule1->SQLQuery1->Close();
}
//---------------------------------------------------------------------------
|
保存,编译,运行。你一定知道“态生两靥之愁,娇袭一身之病”说的哪位了。
“千呼万唤始出来,犹抱琵琶半遮面”,用这句诗来形容我们此时的程序,倒也合适。
和名字及诗一样,图片也已经事先存在数据库中。其中图片的格式是JPEG格式。下面代码更多的是如何显示JPEG格式的图片。
首先在代码前部,#include "dm.h"一行前插入以下新行 #include <jpeg.hpp>
#include <memory> 通过包含该<jpeg.hpp>头文件,用于支持显示JPEG格式的图片。而<memory>则用于帮助我们简化程序需要的内存管理(以后你会发现,这个头文件将是成天陪着我们的好友)。
接下来,在主窗口的CPP源文件最后。加入一个函数,用于支持从数据表中读出JPEG数据,并显示到指定的TImage件上。
ShowJPEGField 函数代码如下:
//---------------------------------------------------------------------------
void ShowJPEGField(TImage* img, TDataSet* dataSet, AnsiString const & fieldName)
{
TField* field = dataSet->FieldByName(fieldName);
using namespace std;
auto_ptr<TStream> stream (dataSet->CreateBlobStream(field, bmRead));
auto_ptr<TJPEGImage> jpeg (new TJPEGImage);
jpeg->LoadFromStream(stream.get());
img->Picture->Assign(jpeg.get());
img->Update();
}
//---------------------------------------------------------------------------
|
最后,我们在ListBox1Click的函数中,调用该函数来显示Jpeg图片。由于ShowJPEGField的实现被我加在代码的最后,这里要调用该函数,还需在调用之前,声明该函数。部分
原有代码,我用省略号表示。
//---------------------------------------------------------------------------
void __fastcall TForm1::ListBox1Click(TObject *Sender)
{
……
this->Memo1->Text = DataModule1->SQLQuery1->FieldByName("POEM")->AsString;
void ShowJPEGField(TImage*, TDataSet*, AnsiString const &);
ShowJPEGField(this->Image1, DataModule1->SQLQuery1, "PHOTO");
DataModule1->SQLQuery1->Close();
}
//---------------------------------------------------------------------------
|
重要声明:12钗的图片均来自网上,其版权归原作者“薇薇”所有。在此特别谢谢这位未知名的网友(画得太美了)。

(运行结果,这是宝钗)
(转到 Turbo
C++版本)
4.4.8 创建数据模块
选择C++ Builder主菜单“File->New->Data Module”。
IDE自动我们创建一个Data
Module,看起来它和一个普通窗口类似——事实上,在运行期时,并不存这一个窗口,IDE这样做,仅仅是为了方便我们往它上面添加一些数据库访问控件,也就是前面所谈的“中间层”。
检查一下,Data Module的Name属性应为默认的DataModule1,如果不是,将它改成是。
按Ctrls
+ S,将新建的文件保存为:dm.cpp。
从“InterBase”控件页上,选择“TIBDatabase、TIBTransaction、TIBQuery”三个控件,加入Data
Module。

(三个IB控件)
假设加入以后,三个控件Name分别为:IBDatabase1、IBTransaction1、IBQuery1。
Step1:设置IBDatabase1属性:
| 属性 |
值 |
说明 |
| DatabaseName |
localhost:E:\白话CPP练习\DBDemo\DEMO_DB.FDB |
“localhost:”之后输入您在4.3节保存数据库文件的路径。 |
IBDatabase还有个属性,紧挨在DatabaseName之上:Connnected。在设置完DatabaseName之后,您现在可以将Connected改为true.应该看到一个登录对话框,User
Name输入sysdba;Password输入masterkey;如果正常登录,则表明设置成功。然后您再将Connected恢复为false。
Step2:设置IBTransaction1属性:
| 属性 |
值 |
说明 |
| DefaultDatabase |
IBDatabase1 |
|
Step3:设置IBQuery1属性:
| 属性 |
值 |
说明 |
| Transaction |
IBTransaction1 |
|
4.4.9 登录数据库
刚才我们已经手工通过修改IBDatabase1的Connected为真,来尝试过连接了,是不是?现在我们用代码来实现这一过程。 切换到4.2步骤中的主界面单元,并切到其CPP文件(本例中保存为main.cpp)。在该源文件中比较靠前的位置,找到#include
"main.h" 这一行前加入两行: #include <DBLogDlg.hpp>
#include "dm.h"
注意引号为半角英文字符。 再切换到主界面,双击“进入大观园”按钮,IDE切换main.cpp中,并且自动添加按钮OnClick的事件,输入一行代码,用于连接到我们的数据库。
void __fastcall TForm1::Button1Click(TObject *Sender)
{
if (LoginDialog != NULL)
DataModule1->IBDatabase1->Connected = true;
}
//---------------------------------------------------------------------------
|
|
代码中,如果没有“if (LoginDialog !=
NULL)”这一行,运行时,点击登录按钮之后,会得到一个异常消息。原因是我们采用了默认的数据库登录对话框,而CB6连接程序的一个小BUG,不主动连接和这个默认的登录对话框的代码,而当程序要用到这个对话框,就迟了……
LoginDiaLog是一个函数,我们故意去判断它的地址是否为空,等于是强制连接程序进行前述连接工作。
|
保存全部。然后编译,运行。点击“进入大观园”……出现一个对话框?前面提到过Firebird/Interbase数据库的默认密码,没印象?肯定是你没认真细读课程了,回头找找吧。输入用户名和密码,我们来到大观园啦……可是,好像什么反应也没有啊?
下面的代码就是如何请出金陵十二钗了…… 4.4.10 列出名字
继续前面Button1Click的函数代码:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
if (LoginDialog != NULL)
DataModule1->IBDatabase1->Connected = true;
DataModule1->IBQuery1->SQL->Text = "SELECT NAME FROM GoldenGirls12";
DataModule1->IBQuery1->Open();
while (!DataModule1->IBQuery1->Eof)
{
AnsiString girlName = DataModule1->IBQuery1->FieldByName("NAME")->AsString;
this->ListBox1->Items->Add(girlName);
DataModule1->IBQuery1->Next();
}
DataModule1->IBQuery1->Close();
}
//---------------------------------------------------------------------------
|
代码中,除了前面那句用于将SQLConnection1的连接属性打开以外,后面的代码做什么呢?我们这简单解释一下。
首先是设置IBQuery1的SQL内容:SQL->Text = ...; 然后打开该查询:IBQuery1->Open();
就这样两句,我们现在已经查询数据库里的十二位古典美女的名字,接下来就是如何在列表框中列出这12个名字。代码中有一个循环语句,依次读出每条记录的“NAME”字段的值,并加入ListBox1中。
最后一行代码用于关闭查询结果。 保存成果。运行,您知道金陵十二钗最后一位是谁吗? 4.4.11 显示介绍
我们的目标是:点我们选中列表框中的某一名字时,左边的Image1控件可以显示该美女的图片,而右上角的Memo1显示介绍其人的诗。显示诗比较简单一些,先来解决它。
和按钮/Button类似,列表框/ListBox也有OnClick事件。当用户选中不同行时,这一事件就会发生。在Form上选中ListBox1,双击,切换到事件代码,编辑代码:
//---------------------------------------------------------------------------
void __fastcall TForm1::ListBox1Click(TObject *Sender)
{
int selectedItem = this->ListBox1->ItemIndex;
if (-1 == selectedItem)
{
return;
}
AnsiString girlName = this->ListBox1->Items->Strings[selectedItem];
DataModule1->IBQuery1->SQL->Text = AnsiString("SELECT * FROM GoldenGirls12 WHERE NAME = ")
+ "'" + girlName + "'";
DataModule1->IBQuery1->Open();
if (DataModule1->IBQuery1->Eof) //没查着?不会吧??
{
return;
}
this->Memo1->Text = DataModule1->IBQuery1->FieldByName("POEM")->AsString;
DataModule1->IBQuery1->Close();
}
//---------------------------------------------------------------------------
|
保存,编译,运行。你一定知道“态生两靥之愁,娇袭一身之病”说的哪位了。 4.4.12 显示图片
和名字及诗一样,图片也已经事先存在数据库中。其中图片的格式是JPEG格式。下面代码更多的是如何显示JPEG格式的图片。
首先在代码前部,#include "dm.h"一行前插入以下新行: #include <jpeg.hpp>
#include <memory> 通过包含该<jpeg.hpp>头文件,用于支持显示JPEG格式的图片。而<memory>则用于帮助我们简化程序需要的内存管理(以后你会发现,这个头文件将是成天陪着我们的好友)。
接下来,在主窗口的CPP源文件最后。加入一个函数,用于支持从数据表中读出JPEG数据,并显示到指定的TImage件上。
//---------------------------------------------------------------------------
void ShowJPEGField(TImage* img, TDataSet* dataSet, AnsiString const & fieldName)
{
请参看Turbo C++版的同名函数。
}
//---------------------------------------------------------------------------
|
最后,我们在ListBox1Click的函数中,调用该函数来显示Jpeg图片。由于ShowJPEGField的实现被我加在代码的最后,这里要调用该函数,还需在调用之前,声明该函数。部分
原有代码,我用省略号表示。
//---------------------------------------------------------------------------
void __fastcall TForm1::ListBox1Click(TObject *Sender)
{
……
this->Memo1->Text = DataModule1->IBQuery1->FieldByName("POEM")->AsString;
void ShowJPEGField(TImage*, TDataSet*, AnsiString const &);
ShowJPEGField(this->Image1, DataModule1->IBQuery1, "PHOTO");
DataModule1->IBQuery1->Close();
}
//---------------------------------------------------------------------------
|
4.4.13 运行结果
参看Turbo C++版运行结果。
作业:
- 本课程中,读取数据表时,被设计为如下步骤:
1)首先读出全部“金陵12钗”的姓名和相关诗,并显示姓名在列表中。
2)当用户选中指定的姓名时,再去查询,读取该人的图片数据。
存在另一种设计方案:一次性将12钗的姓名、诗、图片全部数据都读出。请比对这两种方案各自的优缺点。 |