教程 - 软件 - 文章 - 论坛

::白话C++::

更多感受-Hello Database

作者:南郁(nanyu) www.d2school.com

学习本节课程,要求您已经通过预备课程安装好Firebird。请参考:《Firebird 数据库安装》

程序就是处理数据的过程。而数据,无论处理之前或或处理之后的,往往都需要保存在程序文件之外。通常有两种形式:一种是普通的文件。比如你用Word写了一篇文章,写完后可以保存成一个Word文档。另一种,就是存入数据库。当然,数据库也必然要以某种形式存放在磁盘上;但当我们谈“数据库”时,往往是指一个完整的“数据库系统”。除了包括用于存储数据的文件,“数据库系统”本身还包括提供一些服务程序,用来管理数据。

相比普通的文件系统,数据库系统的最基础的作用体现在以下几点:

  1. 海量数据的管理。包括进行合适的内容组织方式以支持快速存储、查找。这里“数据”不仅仅指实质的数据,还包括数据之间的逻辑关系。
  2. 数据访问的安全机制,包括提供用户权限机制。
  3. 数据并发访问支持。

不过,我们今天的Hello Database仅仅需要完成简单的“感受”。我们将访问一个现成的数据库中的一张数据表。表名为:“GoldenGirls12”——表里面 存有12位绝色美女的“相片”。我们的主要目标就是从数据库中读出相片,并显示在界面即可。在这一过程中,我们会“感受”到:

  1. 如何连接一个数据库。
  2. 如何通过SQL语句来查询指定表中的记录。
  3. 如何处理类似相片这样的二进制数据字段。
  4. 由于照片是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数据库。但记住,真实的情况是:我们对桌面数据库应用完全不感兴趣,我们写的例子事实上还是一个“网络数据库应用”,如果您有两台联网的机器,那么将数据库系统安装在单独的一台机器上,立即就是一个网络数据库应用。

在后面的课程中,大家不仅要完成课程的代码,还必须于代码中体会上图中的分层如何在体现。

4.2 基本界面设计

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”。

4.3 准备数据库文件

点击此处下载本课程示例数据库文件,解压后,将它保存硬盘上某个目录内。

不要选择存放在根目录。另外,有些人图简单,喜欢将下载的文件都“扔”在桌面上,然后不去整理。这应当被看成是一种“恶习” :( 。

网上还有人说Firebird的数据库文件如果存入在带汉字的目录内,会有问题,不过我似乎没有遇上过,这里仅做提醒。

下面的例子,都假设完整的例子数据库文件路径为:“E:\白话CPP练习\DBDemo\DEMO_DB.FDB”。在做练习时,请替换为您所保存的目录。

4.4 数据模块设计

有了数据库文件之后,如何访问该文件呢?这相当于中间层的一部分。下面我们区分TC和CB进行说明。如果您有时间将两个版本的设计进行对比,会发现其实二者的区别很小。

----Turbo C++----

转到 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();
}
//---------------------------------------------------------------------------

保存,编译,运行。你一定知道“态生两靥之愁,娇袭一身之病”说的哪位了。

4.4.6 显示图片

“千呼万唤始出来,犹抱琵琶半遮面”,用这句诗来形容我们此时的程序,倒也合适。

和名字及诗一样,图片也已经事先存在数据库中。其中图片的格式是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();
}
//---------------------------------------------------------------------------

 

4.4.7 运行结果

重要声明:12钗的图片均来自网上,其版权归原作者“薇薇”所有。在此特别谢谢这位未知名的网友(画得太美了)。

(运行结果,这是宝钗)

 

----C++ Builder----

转到 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. 本课程中,读取数据表时,被设计为如下步骤:

1)首先读出全部“金陵12钗”的姓名和相关诗,并显示姓名在列表中。

2)当用户选中指定的姓名时,再去查询,读取该人的图片数据。

存在另一种设计方案:一次性将12钗的姓名、诗、图片全部数据都读出。请比对这两种方案各自的优缺点。

版权所有 谢绝复制。作者:南郁(nanyu) www.d2school.com