第1章 药品信息管理系统

随着计算机技术的不断发展,数据库方面的应用也显得越来越重要,特别是在当前一些十分重要的领域,如银行的账目管理、公司的财政管理等。

医院或医疗机构往往需要对药品进行有效的管理。当前一些小的医疗机构在药品管理上仍然采用人工管理的方法,不仅浪费大量的人力、物力,而且还在很大程度上制约了这些机构的发展,因此有必要采用一种快速高效的管理手段,“药品信息管理系统”就是在这种前提下提出的。

本药品信息管理系统只实现了简单的药品管理功能,读者可以在此基础上进一步地开发与完善。

本章的学习重点:

◆ 基于ODBC数据源的VC数据库编程。

◆ 基于用户的权限分配。

◆ 基本的数据库操作。

1.1 开发背景

东方学院诊所是一个主要实现常规药品的出售,以及简单诊疗活动的小型诊所。对药品的管理以前主要采用手工记账的方式,给管理工作带来了很大的难度,特别是很难及时发现、清除一些缺货和过期的药品。正是基于此,开发了这个小型的药品信息管理系统,实现了对药品信息和相关事务的管理和维护,快速高效地完成库存药品信息的查询、药品的入库、药品出售和药库的清理工作。

1.2 系统分析

1.2.1 需求分析

本产品主要需求就是对药品信息进行基本的管理,用户需要及时准确地掌握该诊所内现有的药品的基本信息(主要包括数量、单价、是否过期等)。同时,用户要求操作界面友好、操作简单方便,对数据库的操作安全可靠,药品从数据库中修改了,但还未结算时断电,数据库应该能恢复。

1.2.2 功能分析

系统需要实现的主要功能包括查询、删除、增加、修改四部分,各功能具体描述如表1-1所示。

表1-1 系统的主要功能分析

1.3 系统设计

通过对药品信息管理系统认真的理解和分析,采用问题分解、自顶向下逐步求精、模块化、信息隐藏等方法,以达到对药品管理系统在功能、性能、行为、设计约束上的全面、一致、准确的理解,指导软件的设计和需求分析员、设计人员、用户之间的交流与合作。

1.3.1 绘制用例图设计系统功能

药品信息管理系统一共包含3种用户角色,分别是医生、售药员和管理员,下面就来分析这三个角色所对应的用例图。

从前面的系统分析可知,医生、售药员和管理员三个角色的权限分别由低到高。医生角色具有的权限是可以查询库存药品信息、提供基本的用户管理功能(如更改密码、更换用户),其用例图如图1-1所示。

图1-1 医生用例图

售药员的权限在医生权限的基础上,还包含药品管理的药品销售功能,其用例图如图1-2所示。

图1-2 售药员用例图

管理员具有所有的权限,包括查询药品、增加药品条目、出售药品、清理药库(包括清除数量为0的药品、清除过期药品等)及所有的用户管理功能,其用例图如图1-3所示。

图1-3 管理员用例图

1.3.2 绘制系统流程图

本系统需要对用户身份进行验证,验证通过后再判断用户是属于医生、售药员或是管理员中的哪一个角色,根据角色判断用户可以使用系统中的相应操作功能。这里以管理员角色为例,其系统流程图如图1-4所示。

图1-4 系统流程图

1.3.3 开发工具和开发技术的选择

开发工具采用的是Microsoft的Visual C++ 6.0开发系统。

工程为基于MFC的单文档工程,创建Windows标准界面,采用窗口分隔技术分别显示用户信息和药品信息。各功能模块的实现,用户可以通过菜单项或者工具按钮来调用,采用弹出对话框的形式,供用户输入操作。

VC数据库开发技术,系统采用MFC提供的ODBC类的CRecordset类来实现。CRecordset类对象提供了从数据源中提取出的记录集。另外,还封装了大部分操作数据库的方法,包括浏览、修改记录,控制游标移动,排序等操作,使用它来开发简单的数据库应用程序十分方便。

1.3.4 系统的运行环境

系统可以直接在Win98、Win2000、WinXP环境下运行。运行前,需要通过ODBC设置数据源,即将Access数据库文件设置为数据源,名称为“medicine”。这里以WinXP系统为例,具体设置如下。

(1)在Windows XP下打开“控制面板”窗口,双击“管理工具”图标,在“管理工具”窗口中双击“数据源(ODBC)”图标,打开“ODBC数据源管理器”对话框。

(2)在弹出的“ODBC数据源管理器”对话框中打开“系统DNS”选项卡,单击“添加”按钮,打开“创建新数据源”对话框。在此对话框中双击“Microsoft Access Driver(*.mdb)”选项,打开“ODBC Microsoft Access安装”对话框,如图1-5所示。

图1-5 “ODBC Microsoft Access安装”对话框

(3)在“ODBC Microsoft Access安装”对话框中“数据源名”文本框内输入 “medicine”。单击“选择”按钮,找到数据库文件“medicine.mdb”。

(4)单击“确定”按钮,返回到“ODBC Microsoft Access安装”对话框。单击“确定”按钮,即完成ODBC的创建工作,如图1-6所示。

图1-6 “ODBC数据源管理器”对话框

1.3.5 系统演示

通过ODBC设置数据源后,即可运行系统。首先弹出“用户登录”对话框,如图1-7所示,用户输入正确的账号、密码和类别后,即可进入系统主界面。

图1-7 “用户登录”对话框

系统主界面将客户窗口视图分隔为左右两部分,左边视图以树形控件的形式显示系统用户的信息,右边视图以列表框的形式显示药品信息,如图1-8所示。

图1-8 医药信息管理系统的主界面

进入主界面,用户就可以通过菜单或者工具按钮进行相关操作。这里以管理员进行药品入库操作为例,进行演示。

当操作员执行“药品管理”→“药品入库”菜单命令时,会弹出如图1-9所示的输入对话框,供用户输入药品信息。

图1-9 药品入库输入对话框

输入完毕,单击“入库”按钮,药品信息即被添加到系统数据库。此时退出对话框,在系统主界面的右侧视图即可看到新添加的药品记录,如图1-10所示。

图1-10 药品入库完毕,主界面窗口

其他的操作与此类似,都是在操作过程中弹出对话框接收用户的输入信息,而后在主界面的视图窗口中显示操作结果。

1.3.6 系统类库设计

药品信息管理系统中的类库主要设计如下。

● 药品管理类CManageMedic:用于对药库进行管理,主要包括药品的入库、药品的出售、药库的清理等,以及为实现这些函数而加的成员函数。

● 药品查询类CMedicSearch:(其实该类可以在CManageMedic中实现,但为了便于实现查询内容比较多的实际情况,将它作为一个类来实现)其成员函数主要包括一般查询、查询库中数量为0的药品、查询过期药品等。

● 用户管理类CUser:主要设立了为完成以下功能的成员函数,如新增用户、删除用户、查询用户信息、修改用户密码等。

● 药品类CMedic:相当于一个结构体,只是为了给系统提供一个数据类型,以对药品进行必要的管理,该类没有成员函数(该类一般作为CManageMedic和CMedicSearch类的一个成员变量)。

● 出售单类CSale:主要是给系统提供一个数据类型,以对药品出售进行有效的管理,该类也没有成员函数(该类作为CManageMedic类的一个成员变量)。

● CMedicSet、CUserSet、CSaleSet类:用于与数据库中的表相连,它们从CRecordSet继承而来。

● 一些对话框类:该系统许多地方都用到了对话框,这类对话框主要有CLogInDlg、CChangPasswordDlg、CUserChangDlg、CStoreDlg、CCleanUpDlg、CSaleDlg等。

● 系统本身提供的框架类:如CMainFrm、CMedicAdminDoc、CMedicAdminApp、CMedicAdminView等。

主要类之间继承关系,如图1-11所示。

图1-11 主要类之间继承关系

1.4 数据库分析与设计

1.4.1 数据库分析

将医药管理系统设计为一个简单的单机应用的小型管理系统,考虑到数据备份、软件移植及维护的便捷性,采用Microsoft提供的Access数据库系统。

1.4.2 数据库概念设计

本系统一共设计规划出三个实体,分别是用户信息实体、药品信息实体、药品销售实体。

用户信息实体用于记录系统登录用户的信息,包括账号、密码、权限等相关信息,其E-R图如图1-12所示。

图1-12 用户信息实体E-R图

药品信息实体是数据库的核心,记录了所存储的药品的基本信息,其E-R图如图1-13所示。

图1-13 药品信息实体E-R图

药品销售实体则记录了药品销售的相关信息,主要指药品的出售数量、出售总价等,其E-R图如图1-14所示。

图1-14 药品销售实体E-R图

1.4.3 数据库逻辑结构设计

根据设计好的各实体E-R图创建数据库的逻辑结构,数据库各表的结构如下。

药品基本信息表(medicine)对应于药品信息实体,记录药品的基本信息,共包含10个字段,如表1-2所示。

表1-2 medicine数据表字段描述

用户信息表(user)对应于用户信息实体,主要用于记录操作用户的基本信息,共包含4个字段,如表1-3所示。

表1-3 user数据表字段描述

saleTable表是在药品销售操作时,存放销售药品信息的临时表。当用户选择药品进行销售时,将药品信息写入该表,当结账成功或者取消销售操作时,将根据表中数据修改medicine表中相应库存值,而后清空该临时表。saleTable表共包含4个字段,如表1-4所示。

表1-4 saleTable数据表字段描述

1.4.4 数据库的创建

各表设计完毕后就可以创建数据库。在Access 2000中,首先创建数据库,数据库名为“medicine”,然后就可以在该数据库中创建各表,如图1-15所示。创建完毕后,即得到数据库文件“medicine.mdb”。

图1-15 在Access中创建数据库表

1.5 公用模块设计

为了节省系统资源,实现代码重用,提高程序运行速度,可以将一些公用的数据信息放到模块中。本系统自定义了CManageMedic、CMedicSearch、CSale、CUser和CMedic类模块,另外还从CRecordSet继承了CMedicSet、CUserSet、CSaleSet类,连接数据库中对应的表。

1.5.1 药品类CMedic

药品类CMedic用于记录药品信息表中的各字段,主要包含一些成员变量,无成员函数。

例程1.1代码位置:光盘\第1章\ MedicAdmin\ Medic.h

    01   class CMedic
    02   {
    03   public:
    04       CMedic();                          //构造函数
    05       virtual~CMedic();                    //析构函数
    06   public:
    07       CString   m_MedicineID;              // 药品代码
    08       CString   m_MedicineName;           // 药品名称
    09       CString   m_MedicineClassification;      // 药品类别
    10       CTime   m_ProduceDate;             // 出产日期
    11       CString   m_Produceplace;             // 产地
    12       int      m_MedicineNumber;          // 库存数量
    13       CString   m_Description;              // 药品简介
    14       float     m_UnitPrice;               // 单价
    15       int      m_QeulityAssurancePeriod;     // 保质期
    16       CString   m_ProduceCompany;         // 生产公司
    17   };

在第7~16行代码定义的变量与medicine数据表字段(如表1-2所示)相对应。

1.5.2 出售单类CSale

出售单类CSale记录saleTable数据表中的字段值,主要包含一些成员变量,无成员函数。

例程1.2代码位置:光盘\第1章\ MedicAdmin\ Sale.h

    01   class CSale
    02   {
    03   public:
    04       CSale();                           //构造函数
    05       virtual~CSale();                     //析构函数
    06   public:
    07       CString   m_MedicineID;              // 药品代码
    08       CString   m_MedicineName;           // 药品名称
    09       int      m_MedicineNumber;          //出售药品数量
    10       float     m_MedicinePrice;            //出售药品的总价
    11   };

在第7~10行代码定义的变量与saleTable数据表字段(如表1-4所示)相对应。

1.5.3 药品管理类CManageMedic

药品管理类CManageMedic实现药品的操作功能,包括药品入库、药品出售和药品处理等,其成员变量和成员函数的定义如下。

例程1.3代码位置:光盘\第1章\ MedicAdmin\ ManageMedic.h

    01   class CManageMedic
    02   {
    03   //定义的成员函数
    04   public:
    05       void Import();                       // 药品入库
    06       float CheckOut();                     // 药品出售
    07       void DeleteOverdueMedic();             // 删除过期药品
    08       void DeleteAllZeroNumber();            // 删除数量为0的药品
    09       //以下成员函数都是为了实现上述函数而设立的
    10       CTime GetExpireTime();               //计算过期时间
    11       BOOL Recover();                    //恢复数据
    12       BOOL DeleteFromSaleTable();           // 从saleTable表中删除记录
    13       BOOL ModifyFromSaleTable();          // 从saleTable表中修改记录
    14       BOOL AddToSaleTable();              // 向saleTable表中添加记录
    15       void SetSaleSet(CSaleSet*theSaleSet);     //设置CSaleSet对象
    16       void SetSale(CSale*theSale);            //设置CSale对象
    17       void SetMedicSet(CMedicSet*theMedicSet);// 设置theMedicSet对象
    18       void SetMedic(CMedic*theMedic);       // 设置theMedic对象
    19       CManageMedic();                    //构造函数
    20       virtual~CManageMedic();              //析构函数
    21   //定义的成员变量
    22   protected:
    23       CMedic*m_theMedic;                 // 用户输入的药品信息
    24       CMedicSet*m_theMedicSet;    //药品的数据库记录集指针,用于访问数据库
    25       CSale*m_theSale;                    // 用户输入的出售药品的信息
    26       CSaleSet*m_theSaleSe;               // 指向出售单数据库记录集指针
    27   };

在第5~8行代码定义的四个函数为CManageMedic类的主要功能函数,而在第15~18行代码定义的四个设置函数分别用于实现对第23~26行代码定义的四个成员变量赋值。

1.5.4 药品查询类CMedicSearch

药品查询类CMedicSearch实现药品的各种查询功能,其成员变量和成员函数的定义如下。

例程1.4代码位置:光盘\第1章\ MedicAdmin\ MedicSearch.h

    01   class CMedicSearch
    02   {
    03   public:
    04       void SearchOverdueMedic();            // 查询过期药品
    05       void SearchZeroNumber();              // 查询数量为0的药品
    06       void SearchAll();                     // 查询所有库存药品
    07       void Search(CString str,int flag);          // 按特定方式查询
    08       //以下成员函数是为了上述主要成员函数的实现而设立的
    09       void SetMedicSet(CMedicSet*theMedicSet); //设置CMedicSet对象
    10       void SetListCtrl(CListCtrl*theListCtrl);//设置CListCtrl对象
    11       CMedicSearch();                     //构造函数
    12       virtual~CMedicSearch();               //析构函数
    13   protected:
    14       CMedicSet*m_theMedicSet;    // 指向药品的数据库记录集指针
    15       CListCtrl*m_theListCtrl;      // 列表控件指针,显示数据
    16   };

在第4~7行代码定义的四个查询函数为CMedicSearch类的主要功能函数,其中第7行代码定义的Search函数,其参数flag=0, 按药品代号查询;flag=1,按药品名称查询;flag=2,按药品类别查询。在第9、第10行代码定义的两个设置函数分别用于实现对第14、第15行代码定义的两个成员变量赋值。

1.5.5 用户管理类CUser

用户管理类CUser实现用户的管理功能,包括用户登录、注册用户、查询用户、更换用户,修改密码等,其成员变量和成员函数的定义如下。

例程1.5代码位置:光盘\第1章\ MedicAdmin\ User.h

    01   class CUser
    02   {
    03   //成员函数
    04   public:
    05       void ModifyPassword(CUserSet*theSet);                //修改用户密码
    06       void SeekUserInfo(int typeRadio,CString str,CUserSet*theSet);//查询用户信息
    07       void DeleteUser(CUser*user,CUserSet*userSet);          //删除用户
    08       BOOL AddNewUser(CUser*newUser,CUserSet*theSet);    //新增用户
    09       BOOL LogIn(CUserSet*);                           //用户登录
    10       //以下成员函数是为了上述主要成员函数的实现而设立的
    11       CString GetPassword();                            //获取用户密码
    12       CString GetAccount();                             //获取用户账号
    13       void SetPassword(CString password);                   //设置用户密码
    14       CString GetType();                                //获取用户类别
    15       void SetAccount(CString account);                     //设置用户账号
    16       void SetAllMember(CString account,CString password,CString type,CString name);
    17       CUser();                                       //构造函数
    18       virtual~CUser();                             //析构函数
    19   //成员变量
    20   protected:
    21       CString m_userAccount;                            //用户账号
    22       CString m_userPassword;                           //用户密码
    23       CString m_userName;                              //用户姓名
    24       CString m_userType;                              //用户类别
    25   };

在第5~9行代码定义的五个操作函数为CUser类的主要功能函数,实现了有关用户管理的核心功能,其他函数和成员变量均是为了配合这几个函数的实现而设计的。

有关各成员函数的详细设计,在下面会结合具体功能实现给出。

1.5.6 记录集类CMedicSet、CUserSet和CSaleSet

记录集类CMedicSet、CUserSet和CSaleSet是以CRecordSet类为基类创建的,分别连接数据库中的medicine表、user表和saleTable表。通过使用Visual C++ 6.0提供的类创建向导对话框,很容易实现这三个类的创建。下面以CMedicSet类为例,简单介绍其创建过程。

在Visual C++ 6.0中,执行“Insert”→“New Class”菜单命令,弹出“New Class”对话框,在其中设置创建的类CMedicSet和基类CRecordSet,如图1-16所示。

图1-16 “New Class”对话框

单击“OK”按钮,会弹出如图1-17所示的“Database Options”对话框,在其中选择ODBC数据源,单击“OK”按钮,弹出如图1-18所示的“Select Database Tables”对话框,在其中选择记录集对应的表medicine,单击“OK”按钮,即实现了CMedicSet类的创建。

图1-17 “Database Options”对话框

图1-18 “Select Database Tables”对话框

1.6 主界面设计

系统采用标准的单文档程序界面,并采用窗口分隔技术将客户窗口视图分隔为左右两部分,分别用于显示用户信息和药品信息。

1.6.1 药品信息视图类CMedicListView

以CListView类为基类,创建新的列表视图类CMedicListView,通过列表的形式显示药品信息。在CMedicListView类的OnCreate函数中,创建列表项和表头,主要代码如下。

例程1.6代码位置:光盘\第1章\ MedicAdmin\ MedicListView.cpp

    01   int CMedicListView::OnCreate(LPCREATESTRUCT lpCreateStruct)
    02   {
    03       if(CListView::OnCreate(lpCreateStruct)==-1)
    04           return-1;
    05       theListCtrl=&GetListCtrl();                      //获取视图对应的列表控件
    06       ModifyStyle(0,LVS_REPORT);                  //设定列表的风格为报表
    07       ASSERT(GetStyle()&LVS_REPORT);
    08       //定义各列表头数组
    09       CString medicField[9]={"药品代码","药品名称","药品类别","库存数量",
    10                       "出产公司","产地","出产时间","保质期(月)","单价(元)"};
    11       for(int i=0;i<8;i++)                           //创建第8列
    12       {
    13           theListCtrl->InsertColumn(i,medicField[i],LVCFMT_LEFT,70);//创建表列
    14       }
    15       theListCtrl->InsertColumn(8,medicField[8],LVCFMT_LEFT,58);   //创建第9列
    16       return 0;
    17   }

在第5行代码通过GetListCtrl函数获取列表视图对应的列表控件指针,通过该指针就可以操作列表控件;第6、第7行代码设置列表控件的风格为报表形式;第9~15行代码实现向列表控件中添加列,并设置列的标题和宽度。

1.6.2 用户信息视图类CUserTreeView

以CTreeView类为基类,创建新的树形视图类CUserTreeView,以树的形式显示用户类别及各类别下注册的用户。在CUserTreeView类的PreCreateWindow函数中,设置树形控件的属性,主要代码如下。

例程1.7代码位置:光盘\第1章\ MedicAdmin\ UserTreeView.cpp

    01   BOOL CUserTreeView::PreCreateWindow(CREATESTRUCT&cs)
    02   {
    03       //通过cs结构的style参数设置树形控件的属性
    04       cs.style|=TVS_HASBUTTONS|TVS_HASLINES|TVS_LINESATROOT;
    05       return CTreeView::PreCreateWindow(cs);
    06   }

在CUserTreeView类的OnInitialUpdate函数中,设置“医生”、“售药员”和“管理员”三个树的根目录,并读取数据库中用户表中的数据,将相应类别的用户代码添加的树到子目录中。

例程1.8代码位置:光盘\第1章\ MedicAdmin\ UserTreeView.cpp

    01   void CUserTreeView::OnInitialUpdate()
    02   {
    03       CTreeView::OnInitialUpdate();
    04       m_theTreeCtrl=&GetTreeCtrl();              //获取视图对应的树形控件
    05       m_theImageList.Create(IDB_TREE_BITMAP,16,1,RGB(0,255,0));//创建图像列表
    06       m_theTreeCtrl->SetImageList(&m_theImageList,TVSIL_NORMAL);//设置图标
    07       CString userTreeHeader[3]={"医生","售药员","管理员"};//根节点标题
    08       HTREEITEM userItem[3];
    09       for(int i=0;i<3;i++)                       /添加三个根节点
    10       {
    11           userItem[i]=m_theTreeCtrl->InsertItem(userTreeHeader[i],0,0,TVI_ROOT);
    12           m_theTreeCtrl->SetItemData(userItem[i],(DWORD)i);//设置节点的指示值
    13       }
    14       CMedicAdminDoc*pDoc=(CMedicAdminDoc*)GetDocument();//获取文档对象
    15       CUserSet*userSet=&pDoc->theUserSet;        //获取数据库中用户记录集
    16       if(userSet->IsOpen())                      //记录集已经打开
    17       {
    18           userSet->Close();                     //关闭记录集
    19       }
    20       userSet->Open();                     //打开记录集
    21       CLogInDlg logInDlg;                      //登录对话框
    22       logInDlg.DoModal();                      //弹出登录对话框
    23       CUser*theUser=&pDoc->theUser;
    24       HTREEITEM thePoint;
    25       for(i=0;i<3;i++)                          //遍历各根节点
    26       {
    27           userSet->m_strFilter="UserClassification='"+userTreeHeader[i]+"'";
    28           userSet->Requery();                   //根据权限查询登录用户
    29           for(int j=0;;j++)                      //遍历查询记录集
    30           {
    31               if(userSet->IsEOF())              //记录集的最后
    32               {
    33                   break;                    /退出
    34               }
    35               if(userSet->m_UserAccount==theUser->GetAccount())  //当前用户
    36               {
    37                   //添加子节点
    38                   HTREEITEM userSpecifics=m_theTreeCtrl->InsertItem(
    39                       userSet->m_UserAccount+"(当前用户)",2,2,userItem[i]);
    40                   m_theTreeCtrl->SetItemData(userSpecifics,(DWORD)j);
    41                   thePoint=userSpecifics;        //当前登录用户位置
    42               }
    43               else                          //不是当前登录用户
    44               {
    45                   //添加子节点
    46                   HTREEITEM userSpecifics=m_theTreeCtrl->InsertItem(
    47                                userSet->m_UserAccount,3,3,userItem[i]);
    48                   m_theTreeCtrl->SetItemData(userSpecifics,(DWORD)j);
    49               }
    50               userSet->MoveNext();             //下一记录
    51           }
    52       }
    53       m_theTreeCtrl->Select(thePoint,TVGN_CARET);  //选中当前登录用户
    54   }

在第4行代码通过GetTreeCtrl函数获取树形视图对应的树形控件指针;第5、第6行代码实现为树形控件设置图像列表;第7~13行代码实现为树形控件添加根节点;第21、第22行代码实现创建并弹出登录对话框;第24~52行代码实现从CUserSet记录集对象中查询各根节点(用户类别)包含的用户账号,并添加为子节点。

需要注意的是,在该函数中实现用户登录对话框CLogInDlg的调用。这样在程序运行时,在主界面创建显示之前,首先会弹出登录对话框。

1.6.3 实现视图窗口的分隔

窗口分隔采用静态分隔的方式。在主框架类CMainFrame的OnCreateClient函数中,通过CSplitterWnd类将客户窗口分隔为左右两部分,并分别对应前面创建的两个视图类,主要代码如下。

例程1.9代码位置:光盘\第1章\ MedicAdmin\ MainFrm.cpp

    01   BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs,
    02                                CCreateContext*pContext)
    03   {
    04       m_wndSplitter.CreateStatic(this,1,2);               //分隔窗口为两列
    05       m_wndSplitter.CreateView(0,0,RUNTIME_CLASS(CUserTreeView),
    06                            CSize(130,600),pContext);  //创建CUserTreeView视图
    07       m_wndSplitter.CreateView(0,1,RUNTIME_CLASS(CMedicListView),
    08                            CSize(700,600),pContext);//创建CMedicListView视图
    09       return TRUE;
    }

其中,m_wndSplitter在头文件中定义的CSplitterWnd类对象如下。

    protected:
    CSplitterWnd m_wndSplitter;

另外,在程序的主界面窗口中,还需要创建相关的菜单命令项和工具按钮,具体创建过程不再详细给出,各菜单项(工具按钮)的消息响应函数在CMainFrame类实现。

1.7 登录模块设计

当程序启动时,在主界面窗口出现之前,首先弹出登录对话框,如图1-7所示。用户需要输入账号、密码等信息,只有通过验证的用户才能进入系统,否则系统自动退出。

登录对话框对应的对话框类为CLogInDlg,在CUserTreeView类的OnInitialUpdate函数中创建和调用,登录对话框确定按钮响应函数OnOK的实现代码如下。

例程1.10代码位置:光盘\第1章\ MedicAdmin\ LogInDlg.cpp

    01   void CLogInDlg::OnOK()
    02   {
    03       CMedicAdminDoc*pDoc=(CMedicAdminDoc*)((CMainFrame*)
    04                   AfxGetMainWnd())->GetActiveDocument(); //获取文档对象
    05       UpdateData(TRUE);                               //获取控件输入
    06       //将对话框输入值传送给文档对象的用户记录集
    07       pDoc->theUser.SetAllMember(m_userAccount,m_userPassword,m_userType,"");
    08       CUserSet*theUserSet=&pDoc->theUserSet;              //获取用户记录集
    09       BOOL flag=pDoc->theUser.LogIn(theUserSet);            //登录验证
    10       if(flag)                                        //验证成功
    11       {
    12           CDialog::OnOK();                            //退出对话框
    13       }
    14       else
    15       {
    16           AfxMessageBox("您无权进入该系统");
    17           ExitProcess(1);                              //结束进程
    18       }
    19   }

在第3、第4行代码中获得文档对象指针,第5~7行代码实现将用户输入的登录信息传送给文档中的CUserSet记录集对象,第8~18行代码实现验证用户的登录权限,并做出相应处理。

在第9行代码中,使用CUser类的LogIn函数对用户的登录信息进行验证,实现代码如下。

例程1.11代码位置:光盘\第1章\ MedicAdmin\ User.cpp

    01   BOOL CUser::LogIn(CUserSet*theSet)
    02   {
    03       theSet->m_strFilter="UserAccount='"+m_userAccount+"'and UserPassword='"
    04           +m_userPassword+"'and UserClassification='"+m_userType+"'";
    05       theSet->Requery();                            //查询数据库user表
    06       if(theSet->GetRecordCount())                    //通过验证
    07       {
    08           return TRUE;                           //返回TRUE
    09       }
    10       return FALSE;                               //验证失败,返回FALSE
    11   }

第3~5行代码根据用户输入的信息在CUser类中进行查询,若查询结果不为空,则表明通过验证。

用户登录成功后,进入系统界面,在左侧的树形窗口会显示当前的登录用户,并依据该用户的类别,具有相应的操作权限。

1.8 查询模块设计

用户可以通过查询菜单项或者工具按钮实现按照药品的名称、类别、名称查询,查询过期药品、查询所有药品和查询库存为0的药品。

1.8.1 按照药品的名称、类别、名称查询模块开发

按照药品的名称、类别、名称查询模块,是通过对话框窗口的形式来提示用户输入查询条件的。以按药品代码查询为例,当用户通过菜单项或工具按钮,执行按药品代码查询时,系统会弹出如图1-19所示的对话框,供用户输入。

图1-19 “按药品代号查询“对话框

在编辑框中输入要查询药品的代号,单击“查询”按钮,相应的查询结果会显示在主界面的列表视图窗口中,如图1-20所示。

图1-20 查询结果对话框

按药品代码查询对话框对应的对话框类为CSearchIDDlg,查询按钮响应函数OnSearchId的实现代码如下。

例程1.12代码位置:光盘\第1章\ MedicAdmin\ SearchIDDlg.cpp

    01   void CSearchIDDlg::OnSearchId()
    02   {
    03       UpdateData(TRUE);                           //获取用户输入
    04       if(m_id=="")                                //用户输入为空
    05       {
    06           AfxMessageBox("请输入要查询的药品代号");
    07           return;                                //返回
    08       }
    09       CMedicSearch medicSearch;                 //CMedicSearch对象
    10       CFrameWnd*pWnd=(CFrameWnd*)AfxGetMainWnd();//获取主窗口指针
    11       CMedicAdminDoc*pDoc=(CMedicAdminDoc*)pWnd->GetActiveDocument();
    12       CMedicSet*theMedicSet=&pDoc->theMedicSet;  //获取文档中的CMedicSet对象
    13       POSITION pos=pDoc->GetFirstViewPosition();       //获取视图位置
    14       while(pos!=NULL)                           //遍历视图
    15       {
    16           CView*pView=pDoc->GetNextView(pos);              //下一视图位置
    17           if(pView->IsKindOf(RUNTIME_CLASS(CMedicListView)))  //列表视
    18           {
    19               CListCtrl*theListCtrl=((CMedicListView*)pView)->theListCtrl;
    20               medicSearch.SetListCtrl(theListCtrl);           //获取列表控件对象
    21               medicSearch.SetMedicSet(theMedicSet);        //设置CMedicSet对象
    22               medicSearch.Search(m_id,0);                //执行查询
    23           }
    24       }
    25       CDialog::OnOK();                                //关闭对话框
    26   }

在函数的第10~12行代码中实现获取文档对象中的CMedicSet对象,第13~16行代码实现获取CMedicListView视图对象,第17~23行代码实现获取视图对象中的列表控件对象,并设置CMedicSearch对象,进行查询。

查询操作是通过CMedicSearch类的Search函数实现的,CMedicSearch类的声明见例程1.4。Search函数的实现代码如下。

例程1.13代码位置:光盘\第1章\ MedicAdmin\ MedicSearch.cpp

    01   void CMedicSearch::Search(CString str1,int flag)
    02   {
    03       if(flag==0)                                     //按药品代码查询
    04       {
    05           m_theMedicSet->m_strFilter="MedicineID='"+str1+"'";
    06       }
    07       else if(flag==1)                                  //按药品名称查询
    08       {
    09           m_theMedicSet->m_strFilter="MedicineName='"+str1+"'";
    10       }
    11       else if(flag==2)                                  //按药品类别查询
    12       {
    13           m_theMedicSet->m_strFilter="MedicineClassification='"+str1+"'";
    14       }
    15       else
    16       {
    17           return;                                    //返回
    18       }
    19       m_theMedicSet->Requery();                     //执行SQL查询语句
    20       m_theListCtrl->DeleteAllItems();                 //删除列表控件中的数据
    21       //下面实现将查询结果中的记录写入列表控件
    22       CString str;
    23       for(int i=0;;i++)                              //遍历查询结果记录集
    24       {
    25           if(m_theMedicSet->IsEOF())                //记录集的最后
    26           {
    27               break;                            //返回
    28           }
    29           m_theListCtrl->InsertItem(i,m_theMedicSet->m_MedicineID);//药品ID
    30           m_theListCtrl->SetItemText(i,1,m_theMedicSet->m_MedicineName);//名称
    31           m_theListCtrl->SetItemText(i,2,m_theMedicSet->m_MedicineClassification);
    32           str.Format("%d",m_theMedicSet->m_MedicineNumber);
    33           m_theListCtrl->SetItemText(i,3,str);                    //数量
    34           m_theListCtrl->SetItemText(i,4,m_theMedicSet->m_ProduceCompany);//公司
    35           m_theListCtrl->SetItemText(i,5,m_theMedicSet->m_Produceplace); //产地
    36           CTime produceTime=m_theMedicSet->m_ProduceDate;
    37           str=produceTime.Format("%B %d,%Y");
    38           m_theListCtrl->SetItemText(i,6,str);                    //生产日期
    39           str.Format("%d",m_theMedicSet->m_QeulityAssurancePeriod);
    40           m_theListCtrl->SetItemText(i,7,str);                    //保质期
    41           str.Format("%f",m_theMedicSet->m_UnitPrice);
    42           m_theListCtrl->SetItemText(i,8,str);                    //单价
    43           m_theMedicSet->MoveNext();                       //下一记录
    44       }
    45   }

在函数的第3~19行代码中根据参数flag的取值,依据不同的字段查询medicine表中的药品记录,得到记录集m_theMedicSet,第22~44 行代码实现依次将记录集m_theMedicSet中的记录添加到列表控件中。

1.8.2 其他查询模块功能开发

查询过期药品、查询所有药品和查询库存为0的药品功能,不需要输入对话框,直接在CMainFrame类中响应执行,并将查询结果显示在列表视图中。如在系统中执行“药品查询”→“查询过期药品”菜单命令,在主界面的列表视图窗口中会显示所有过期药品信息,如图1-21所示。

图1-21 查询结果对话框

下面以查询过期药品为例,讲解其代码实现过程。在CMainFrame类中,查询过期药品命令项响应函数OnSeekOverdue的实现代码如下。

例程1.14代码位置:光盘\第1章\ MedicAdmin\MainFrm.cpp

    01   void CMainFrame::OnSeekOverdue()
    02   {
    03       CMedicSearch medicSearch;                 //构造CMedicSearch对象
    04       CMedicAdminDoc*pDoc=(CMedicAdminDoc*)GetActiveDocument();//文档对象
    05       CMedicSet*theMedicSet=&pDoc->theMedicSet;      //文档中的CMedicSet对象
    06       POSITION pos=pDoc->GetFirstViewPosition();       //获取视图位置
    07       while(pos!=NULL)                           //遍历各视图
    08       {
    09           CView*pView=pDoc->GetNextView(pos);      //下一视图位置
    10           if(pView->IsKindOf(RUNTIME_CLASS(CMedicListView)))//列表视
    11           {
    12               //获取列表视图中的列表控件
    13               CListCtrl*theListCtrl=((CMedicListView*)pView)->theListCtrl;
    14               medicSearch.SetListCtrl(theListCtrl);       //设置列表控件对象
    15               medicSearch.SetMedicSet(theMedicSet);    //设置CMedicSet对象
    16               medicSearch.SearchOverdueMedic();       //查询过期药品
    17           }
    18       }
    19   }

第3~5 行代码获取文档对象中的CMedicSet对象,并在第15 行代码中将其传给CMedicSearch对象,第6~14行代码获取列表视图CMedicListView对象中的列表控件指针,并将其传给CMedicSearch对象,第16行代码通过调用CMedicSearch类的SearchOverdueMedic函数实现查询过期药品并显示在列表控件中。

SearchOverdueMedic函数的实现代码如下。

例程1.15代码位置:光盘\第1章\ MedicAdmin\ MedicSearch.cpp

    01   void CMedicSearch::SearchOverdueMedic()
    02   {
    03       m_theMedicSet->m_strFilter="";
    04       m_theMedicSet->Requery();             //获取所有药品信息表中的数据
    05       m_theListCtrl->DeleteAllItems();                      //清空列表控件
    06       CString str;
    07       CManageMedic manageMedic;                       //CManageMedic对象
    08       for(int i=0;;i++)                                  //开始遍历记录集
    09       {
    10           if(m_theMedicSet->IsEOF())                     //记录集的最后
    11           {
    12               break;                                 //退出
    13           }
    14           manageMedic.SetMedicSet(m_theMedicSet); //设置CManageMedic对象
    15           CTime timeAfter=manageMedic.GetExpireTime();     //获取到期日期
    16           CTime timeNow=CTime::GetCurrentTime();         //获取当前日期
    17           if(timeAfter<timeNow)                         //药品已经过期
    18           {
    19               //下面实现将该记录添加到列表视中的列表控件
    20               m_theListCtrl->InsertItem(i,m_theMedicSet->m_MedicineID);
    21               m_theListCtrl->SetItemText(i,1,m_theMedicSet->m_MedicineName);
    22               ……
    23               str.Format("%f",m_theMedicSet->m_UnitPrice);
    24               m_theListCtrl->SetItemText(i,8,str);
    25           }
    26           m_theMedicSet->MoveNext();                   //下一记录
    27       }
    28   }

在函数的第3、第4行代码实现获取所有药品信息表中的记录,在第8~27行代码中实现遍历这些记录,并将过期药品添加到列表控件中显示,第15~17行代码实现判断药品是否过期,即首先查询数据库中每一种药品的到期时间,然后获取当前系统时间,将当前时间与到期时间(生产日期+保质期)进行比较,如果当前时间大于到期时间则表明该药品已过期。

1.9 药品管理模块设计

药品管理模块主要包括药品入库、药品销售和药品清理三个子模块。

1.9.1 药品入库功能开发

当用户通过菜单项或工具按钮,执行按药品入库命令时,系统会弹出如图1-9所示的对话框,供用户输入。输入完毕后,单击“入库”按钮,即将该药品信息添加到系统数据库中。

药品入库对话框对应的对话框类为CStoreDlg,入库按钮响应函数OnOK的实现代码如下。

例程1.16代码位置:光盘\第1章\ MedicAdmin\ StoreDlg.cpp

    01   void CStoreDlg::OnOK()
    02   {
    03       UpdateData();                               //获取用户输入
    04       if(m_id=="")                                //药品代号为空
    05       {
    06           AfxMessageBox("请输入药品代号!");
    07           return;                                //返回
    08       }
    09       if(m_name=="")                             //药品名称为空
    10       {
    11           AfxMessageBox("请输入药品名称!");
    12           return;                                //返回
    13       }
    14       if(m_type=="")                              //药品类别为空
    15       {
    16           AfxMessageBox("请输入药品类别!");
    17           return;                                //返回
    18       }
    19       //输入的生产日期不正确
    20       if((m_year<=1970)||(m_month>12)||(m_month<1)||(m_day>31)||(m_day<1))
    21       {
    22           AfxMessageBox("请重新输入出产日期!");
    23           return;                                //返回
    24       }
    25       //将用户的输入信息写入CMedic对象
    26       CMedic*medic=new CMedic;                   //CMedic对象
    27       medic->m_MedicineID=m_id;                    //药品代码
    28       medic->m_MedicineName=m_name;               //药品名称
    29       ……                                      //其他字段
    30       medic->m_UnitPrice=m_unitPrice;                //药品单价
    31       CMainFrame*theFrm=(CMainFrame*)AfxGetMainWnd();//主框架指针
    32       //获取文档对象指针
    33       CMedicAdminDoc*pDoc=(CMedicAdminDoc*)theFrm->GetActiveDocument();
    34       CMedicSet*theSet=&pDoc->theMedicSet;  //获取文档对象中的CMedicSet对象
    35       CManageMedic*manageMedic=new CManageMedic;   //CManageMedic对象
    36       manageMedic->SetMedic(medic);                 //设置CMedic对象
    37       manageMedic->SetMedicSet(theSet);               //设置CMedicSet对象
    38       manageMedic->Import();                       //将记录添加到数据库表
    39       //入库完毕,初始化对话框中控件的数据
    40       m_id="";
    41       m_name="";
    42       m_type="";
    43       m_description="";
    44       m_company="";
    45       m_place="";
    46       m_year=2000;
    47       m_day=1;
    48       m_month=1;
    49       m_unitPrice=0;
    50       m_periodQuelity=0;
    51       m_number=0;
    52       UpdateData(FALSE);                      //更新控件数据
    53       //CDialog::OnOK();                       //不退出对话框
    54   }

在函数的第3~24行代码中实现判断用户添加的药品记录信息是否符合输入要求,在第26~30行代码中使用用户输入的药品信息构造CMedic对象,第31~34行代码实现获取文档对象中的CMedicSet对象,第36、37 行代码分别将CMedic对象和CMedicSet对象传送给CManageMedic对象,第38行代码通过调用CManageMedic对象的Import函数实现入库操作。

CManageMedic类的Import函数的实现代码如下。

例程1.17代码位置:光盘\第1章\ MedicAdmin\ ManageMedic.cpp

    01   void CManageMedic::Import()
    02   {
    03       m_theMedicSet->m_strFilter="MedicineID='"+m_theMedic->m_MedicineID+"'";
    04       m_theMedicSet->Requery();                     //按药品代码查询
    05       if(m_theMedicSet->GetRecordCount())                 //药品代码已存在
    06       {
    07           //判断输入的药品代码与数据库中存储的药品名称是否匹配
    08           if(m_theMedic->m_MedicineName!=m_theMedicSet->m_MedicineName)
    09           {
    10               AfxMessageBox("输入的代码和名称不匹配!");
    11               return;                                //返回
    12           }
    13           //判断输入的药品代码与数据库中存储的药品类别是否匹配
    14           if(m_theMedic->m_MedicineClassification!=
    15                       m_theMedicSet->m_MedicineClassification)
    16           {
    17               AfxMessageBox("输入的代码和类别不匹配!");
    18               return;                                //返回
    19           }
    20           //更新库存数量
    21           m_theMedicSet->Edit();
    22           m_theMedicSet->m_MedicineNumber+=m_theMedic->m_MedicineNumber;
    23           if(m_theMedicSet->CanUpdate())             //判断记录集是否可更新
    24           {
    25               m_theMedicSet->Update();              //更新记录
    26           }
    27       }
    28       else
    29       {
    30           m_theMedicSet->AddNew();                //添加新记录
    31           m_theMedicSet->m_MedicineID=m_theMedic->m_MedicineID;//药品代码
    32           m_theMedicSet->m_MedicineName=m_theMedic->m_MedicineName;//名称
    33           ……                                 //其他字段
    34           if(m_theMedicSet->CanUpdate())             //判断记录集是否可更新
    35           {
    36               m_theMedicSet->Update();              //更新记录
    37           }
    38           if(m_theMedicSet->IsEOF())                //记录集的最后
    39           {
    40               m_theMedicSet->MoveLast();            //移到最后一条记录
    41           }
    42           m_theMedicSet->Requery();             //查询显示所有记录
    43       }
    44   }

函数的第3、第4行代码根据输入的药品编号在数据库中查询其对应的记录,若有记录,则判断用户输入是否正确。若正确则只需修改药品库信息中的药品数量(第5~27行代码);若没有记录,则将用户输入的信息添加到药品库中(第30~41行代码)。第42行代码实现在列表视中显示入库后所有库存药品信息。

注意,只有管理员才具有药品入库操作权限。具体实现是在CMainFrame类中,为“药品入库”菜单项映射ON_UPDATE_COMMAND_UI响应函数OnUpdateStore,主要代码如下。

例程1.18代码位置:光盘\第1章\ MedicAdmin\ MainFrm.cpp

    01   void CMainFrame::OnUpdateStore(CCmdUI*pCmdUI)
    02   {
    03       //获取文档对象的CUser对象
    04       CUser*theUser=&((CMedicAdminDoc*)GetActiveDocument())->theUser;
    05       if(theUser->GetType()=="管理员")                    //用户为管理员
    06       {
    07           pCmdUI->Enable();                           //激活菜单项
    08       }
    09       else
    10       {
    11           pCmdUI->Enable(FALSE);                      //禁用菜单项
    12       }
    13   }

函数在第4行代码中获取文档对象中的CUser记录集对象,第5~12行代码根据CUser记录集对象的用户权限实现激活或禁用药品入库菜单项。

1.9.2 药品销售功能开发

当用户通过菜单项或工具按钮,执行按药品销售命令时,系统会弹出如图1-22所示的对话框,供用户输入。

图1-22 “药品销售窗口”对话框

当用户填写要销售药品的名称和数量时,单击“增加”按钮,此时系统从药品库中查找待出售的药品,看是否有库存、是否过期。若有库存且没有过期,则从药品库中拿出药品(即修改药品库中药品数量)并加入到出售单中,即在对话框中的列表框中显示。

“药品销售窗口”对话框对应的对话框类为CSaleDlg,“增加”按钮响应函数OnAdd的实现代码如下。

例程1.19代码位置:光盘\第1章\ MedicAdmin\ SaleDlg.cpp

    01   void CSaleDlg::OnAdd()
    02   {
    03       UpdateData();                       //获取控件输入
    04       if(m_name=="")                     //药品名称为空
    05       {
    06           AfxMessageBox("请填入药品名称!");
    07           return;                        //返回
    08       }
    09       if(m_number==0)                            //出售数量为0
    10       {
    11           AfxMessageBox("请输入销售药品数量!");
    12           return;                                //返回
    13       }
    14       CManageMedic manageMedic;                   //CManageMedic对象
    15       CSale*newSale=new CSale;                     //CSale对象
    16       theMedicSet->m_strFilter="MedicineName='"+m_name+"'";
    17       theMedicSet->Requery();                       //根据名称查询药品
    18       if(theMedicSet->GetRecordCount())               //数据库中有该药品记录
    19       {
    20           //将查询记录中的药品信息赋予CSale对象
    21           newSale->m_MedicineID=theMedicSet->m_MedicineID;//编码
    22           newSale->m_MedicineName=theMedicSet->m_MedicineName;//名称
    23           newSale->m_MedicineNumber=m_number;      //出售数量
    24           newSale->m_MedicinePrice=theMedicSet->m_UnitPrice*m_number;//总价
    25           manageMedic.SetSale(newSale);  //将CSale对象赋予CManageMedic对象
    26           manageMedic.SetSaleSet(theSaleSet);
    27           manageMedic.SetMedicSet(theMedicSet);       //赋值CManageMedic对象
    28           CTime expireTime=manageMedic.GetExpireTime();//获取过期日期
    29           CTime nowTime=CTime::GetCurrentTime();     //获取当前时间
    30           if(expireTime<nowTime)                   //药品过期
    31           {
    32               AfxMessageBox("该药品已过期!");
    33               return;                            //返回
    34           }
    35           if(manageMedic.AddToSaleTable())           //完成出售药品操作
    36           {
    37               //将出售药品记录信息添加到列表框中
    38               int number=m_saleList.GetItemCount();//列表框记录数目
    39               CString str;
    40               str.Format("%d",newSale->m_MedicineNumber);  //药品数量
    41               m_saleList.InsertItem(number,newSale->m_MedicineID);//代码
    42               m_saleList.SetItemText(number,1,newSale->m_MedicineName);//名称
    43               m_saleList.SetItemText(number,2,str);      //药品名称
    44               str.Format("%f",newSale->m_MedicinePrice);
    45               m_saleList.SetItemText(number,3,str);      //金额
    46           }
    47       }
    48       else
    49       {
    50           AfxMessageBox("仓库中没有该药品!");
    51       }
    52       //初始化编辑控件
    53       m_name="";
    54       m_number=0;
    55       UpdateData(FALSE);                          //更新控件数据
    56   }

函数的第3~13行代码获取输入的出售药品名称和数量,并验证其是否有效;第16、第17行代码根据输入的药品名称从medicine数据表中查询记录;第18~47行代码实现药品出售操作并将出售药品记录添加到列表框中。出售药品核心数据库操作是通过CManageMedic类的AddToSaleTable函数来实现的(第35行代码),其主要代码如下。

例程1.20代码位置:光盘\第1章\ MedicAdmin\ ManageMedic.cpp

    01   BOOL CManageMedic::AddToSaleTable()
    02   {
    03       m_theSaleSet->m_strFilter="MedicineID='"+m_theSale->m_MedicineID+"'";
    04       m_theSaleSet->Requery();              //从出售单中查询药品代码
    05       if(m_theSaleSet->GetRecordCount())       //结果不为空,出售单中已有该药品
    06       {
    07           AfxMessageBox("该药品已经存在!");
    08           return FALSE;                  //返回
    09       }
    10       m_theMedicSet->m_strFilter="MedicineID='"+m_theSale->m_MedicineID+"'";
    11       m_theMedicSet->Requery();         //从数据库中查询出售药品记录
    12       if(!m_theMedicSet->GetRecordCount())//结果为空
    13       {
    14           AfxMessageBox("仓库中没有该药品!");
    15           return FALSE;                  //返回
    16       }
    17       //库存数量少于出售数量
    18       if(m_theMedicSet->m_MedicineNumber<m_theSale->m_MedicineNumber)
    19       {
    20           AfxMessageBox("该药品库存不足!");
    21           return FALSE;                  //返回
    22       }
    23       //将待出售药品信息写入临时表saleTable中
    24       m_theSaleSet->AddNew();              //添加新记录
    25       m_theSaleSet->m_MedicineID=m_theSale->m_MedicineID;      //编号
    26       m_theSaleSet->m_MedicineName=m_theSale->m_MedicineName;  //名称
    27       m_theSaleSet->m_MedicineNumber=m_theSale->m_MedicineNumber;//出售数量
    28       m_theSaleSet->m_MedicinePrice=m_theSale->m_MedicinePrice;   //总金额
    29       if(m_theSaleSet->CanUpdate())
    30       {
    31           m_theSaleSet->Update();           //更新CSaleSet对象
    32       }
    33       if(!m_theSaleSet->IsEOF())             //记录集的最后
    34       {
    35           m_theSaleSet->MoveLast();         //最后一条记录
    36       }
    37       m_theSaleSet->Requery();              //获取saleTable表所有记录
    38       int number=m_theMedicSet->m_MedicineNumber;//药品数量
    39       m_theMedicSet->Edit();
    40       //相应减少库存数量
    41       m_theMedicSet->m_MedicineNumber=number-m_theSale->m_MedicineNumber;
    42       if(m_theMedicSet->CanUpdate())
    43       {
    44           m_theMedicSet->Update();              //更新CMedicSet
    45       }
    46       return TRUE;
    47   }

函数的第3~22行代码判断要出售的药品信息是否有效,第24~32行代码实现将待出售药品信息写入临时表saleTable中,第38~45行代码实现更新medicine数据表中相关药品的库存数量。

将本次待出售的药品都加入到出售单中后,执行“结算”命令,出售单将被清空,弹出对话框显示出售的总金额并执行结算,“结算”按钮响应函数OnSettlementButton的实现代码如下:

例程1.21代码位置:光盘\第1章\ MedicAdmin\ SaleDlg.cpp

    01   void CSaleDlg::OnSettlementButton()
    02   {
    03       CManageMedic manageMedic;           //CManageMedic对象
    04       manageMedic.SetSaleSet(theSaleSet);      //获取CSaleSet对象
    05       float money;
    06       money=manageMedic.CheckOut();        //计算总金额
    07       CString str;
    08       str.Format("%f",money);               //转换为字符串
    09       if(money)
    10       {
    11           AfxMessageBox("请支付 "+str+" 元!");//弹出计算结果对话框
    12       }
    13       else
    14       {
    15           AfxMessageBox("请填写出售单!");
    16       }
    17   }

函数通过在第6行代码调用CManageMedic对象的CheckOut函数计算出售单中出售药品的总金额,并在第11行代码中弹出对话框显示。

只有售药员和管理员才具有药品销售的权限,具体设置可参照药品入库模块的设计。

1.9.3 药库清理功能开发

当用户通过菜单项或工具按钮,执行按药库清理命令时,系统会弹出如图1-23所示的对话框,用户可以选择查看数量为0的药品或者过期药品。以清理过期药品为例,当在对话框中选中列出过期药品单选按钮后,列表框中将列出所有的过期药品条目。

图1-23 药品清理对话框

单击“清理出库”按钮,系统将会把过期药品或库存数量为0的药品记录从数据库中清除。清理药品对话框对应的对话框类为CCleanUpDlg,“清理出库”按钮响应函数OnCleanUpButton的实现代码如下。

例程1.22代码位置:光盘\第1章\ MedicAdmin\ CleanUpDlg.cpp

    01   void CCleanUpDlg::OnCleanUpButton()
    02   {
    03       UpdateData();                   //获取单选按钮的选中状态
    04       CManageMedic manageMedic;       //CManageMedic对象
    05       manageMedic.SetMedicSet(m_theMedicSet);
    06       if(m_selectRadio==0)             //数量为0的药品
    07       {
    08           manageMedic.DeleteAllZeroNumber();     //清除库存为0的药品
    09       }
    10       else                                  //过期药品
    11       {
    12           manageMedic.DeleteOverdueMedic();      //清除过期药品
    13       }
    14       m_theMedicSet->m_strFilter="";
    15       m_theMedicSet->Requery();             //查询所有药品信息
    16       m_showList.DeleteAllItems();                //清空列表框
    17       for(int i=0;;i++)                          //遍历药品信息记录集
    18       {
    19           if(m_theMedicSet->IsEOF())            //记录集的最后
    20           {
    21               break;                        //返回
    22           }
    23           //依次将药品记录添加到列表框中
    24           m_showList.InsertItem(i,m_theMedicSet->m_MedicineID);   //药品编号
    25           m_showList.SetItemText(i,1,m_theMedicSet->m_MedicineName);//名称
    26           CString str;
    27           str.Format("%d",m_theMedicSet->m_MedicineNumber);
    28           m_showList.SetItemText(i,2,str);                  //药品数量
    29           manageMedic.SetMedicSet(m_theMedicSet);
    30           CTime timeAfter=manageMedic.GetExpireTime();     //获取过期日期
    31           CTime timeNow=CTime::GetCurrentTime();         //当前时间
    32           CString strfool;
    33           if(timeAfter>timeNow)                         //没有过期
    34           {
    35               strfool="否";
    36           }
    37           else                                      //过期
    38           {
    39               strfool="是";
    40           }
    41           m_showList.SetItemText(i,3,strfool);               //显示是否过期
    42           m_theMedicSet->MoveNext();                   //下一记录
    43       }
    44   }

在函数的第8 行代码中实现清除库存为0 的药品,第12 行代码实现清除过期药品,第14~43行代码实现将数据库中的所有药品记录添加到列表框中显示。

函数的第12行代码调用CManageMedic类的DeleteOverdueMedic函数实现从数据库表中删除过期药品记录,主要代码如下。

例程1.23代码位置:光盘\第1章\ MedicAdmin\ ManageMedic.cpp

    01   void CManageMedic::DeleteOverdueMedic()
    02   {
    03       m_theMedicSet->m_strFilter="";
    04       m_theMedicSet->Requery();                 //查询所有药品记录
    05       for(int i=0;;i++)                              //遍历记录集
    06       {
    07           if(m_theMedicSet->IsEOF())                //记录集的最后
    08           {
    09               break;                            //返回
    10           }
    11           CTime expireTime=GetExpireTime();          //计算过期日期
    12           CTime nowTime=CTime::GetCurrentTime();     //获取当前日期
    13           if(expireTime<nowTime)                   //药品已过期
    14           {
    15               m_theMedicSet->Delete();              //删除药品
    16           }
    17           m_theMedicSet->MoveNext();               //下一记录
    18       }

函数的第3、第4行代码实现查询medicine数据表中的所有药品记录,第5~17行代码实现对记录集中的每条记录进行检查,看是否属于过期药品,若是过期药品则将其删除,否则不管。

1.10 用户管理模块设计

系统的用户管理模块包括注册用户、删除用户、查询用户、更换用户、更改密码等操作,其中注册用户和删除用户操作是只有管理员才具有的权限。

用户管理的每一种操作都是通过弹出对话框的形式来实现与用户交互,对话框设计分别如图1-24、图1-25、图1-26、图1-27 和图1-28所示,其对应的对话框类分别为CChangePasswordDlg、CRegisterUserDlg、CUserChangDlg、CDeleteUserDlg和CSeekUserDlg。

图1-24 “更改用户密码窗口”对话框

图1-25 “注册用户窗口”对话框

图1-26 “更换用户窗口”对话框

图1-27 “删除用户窗口”对话框

图1-28 “查询用户信息窗口”对话框

所有用户管理都是通过CUser类提供的成员函数对CUserSet数据集进行操作来实现的。这里以注册用户为例,“注册”按钮响应函数的实现代码如下。

例程1.24代码位置:光盘\第1章\ MedicAdmin\ RegisterUserDlg.cpp

    01   void CRegisterUserDlg::OnOK()
    02   {
    03       UpdateData();                                   //获取控件输入
    04       if(m_registerUserAccount=="")                       //用户账号为空
    05       {
    06           AfxMessageBox("用户账号不能为空");
    07       }
    08       else if(m_registerUserPassword!=m_registerUserPassword2)//密码不一致
    09       {
    10           AfxMessageBox("两次输入的密码不匹配");
    11       }
    12       else if(m_registerUserPassword=="")                  //密码为空
    13       {
    14           AfxMessageBox("为保密起见,请输入密码");
    15       }
    16       else if(m_registerUserType=="")                      //类别为空
    17       {
    18           AfxMessageBox("请选择用户的类别");
    19       }
    20       else
    21       {
    22           CUser*newUser=new CUser;                    //CUser对象指针
    23           //将控件输入信息赋予CUser对象
    24           newUser->SetAllMember(m_registerUserAccount,m_registerUserPassword,
    25                                m_registerUserType,m_registerUserName);
    26           CMedicAdminDoc*pDoc=(CMedicAdminDoc*)((CMainFrame*)
    27                       AfxGetMainWnd())->GetActiveDocument();//获取文档对象
    28           CUser*theUser=&pDoc->theUser;        //获取文档对象中的CUser对象
    29           if(theUser->AddNewUser(newUser,theSet))          //创建新的用户记录
    30           {
    31               //将新创建的用户记录添加到对话框中的列表框
    32               int number=m_registerUserList.GetItemCount();//编剧框中记录数量
    33               m_registerUserList.InsertItem(number,m_registerUserAccount);
    34               m_registerUserList.SetItemText(number,1,m_registerUserPassword);
    35               m_registerUserList.SetItemText(number,2,m_registerUserType);
    36               m_registerUserList.SetItemText(number,3,m_registerUserName);
    37           }
    38           //初始化各控件
    39           m_registerUserAccount="";
    40           m_registerUserPassword="";
    41           m_registerUserPassword2="";
    42           m_registerUserType="";
    43           m_registerUserName="";
    44           UpdateData(FALSE);                      //更新显示控件
    45       }
    46   }

在函数的第3~19行代码中实现获取用户输入的注册用户信息,并验证其有效性,第22~25行代码实现根据用户输入的注册信息构造CUser对象,第26~37行代码利用文档类中的CUser对象在数据库中创建新的用户记录,并添加到列表框中显示。

在第29行代码中,通过CUser类的AddNewUser函数实现将创建的用户记录添加到数据库中,实现代码如下。

例程1.25代码位置:光盘\第1章\ MedicAdmin\ Register\User.cpp

    01   BOOL CUser::AddNewUser(CUser*newUser,CUserSet*theSet)
    02   {
    03       theSet->m_strFilter="UserAccount='"+newUser->GetAccount()+"'";
    04       theSet->Requery();                                //查询该账号
    05       if(theSet->GetRecordCount())                        //记录集不为空
    06       {
    07           AfxMessageBox("该账号已经存在!");
    08           return FALSE;                               //返回
    09       }
    10       theSet->AddNew();                               //添加新记录
    11       theSet->m_UserAccount=newUser->m_userAccount;        //账号
    12       theSet->m_UserPassword=newUser->m_userPassword;      //密码
    13       theSet->m_UserClassification=newUser->m_userType       //类别
    14       theSet->m_UserName=newUser->m_userName;           //用户姓名
    15       if(theSet->CanUpdate())
    16       {
    17           theSet->Update();                            //更新记录集
    18       }
    19       if(!theSet->IsEOF())                               //不是记录集的最后
    20       {
    21           theSet->MoveLast();                      //移动到最后一条记录
    22       }
    23       return TRUE;
    24   }

在函数的第3~9行代码中首先根据新用户的账号查找用户表,若表中已存在该用户,则显示报错信息,否则,在第10~18行代码实现将新用户加入到用户表中。

1.11 开发技巧和难点分析

1.11.1 文档——视图结构

系统创建的是MFC SDI应用程序,采用了窗口静态分隔技术,从而实现了单文档多视图的系统结构。在文档——视图结构的应用程序中,用户通过包含在框架窗口中的视图来浏览和操作文档中的数据。

在文档/视图结构的应用程序框架中,主要包含5个类:CWinApp类(应用程序对象)、CFrameWnd类(主框架)、CView类(视图)、CDocument(文档)类和CDocTemplate类(文档模板)。在SDI应用程序中,以上对象关系如图1-29所示。

图1-29 SDI程序中各对象的关系

应用程序框架中各对象间的关系如下:

● 文档对象存储着与该文档相关联的视图对象的列表,以及创建文档时所使用的文档模板的指针。

● 视图是框架窗口的子窗口,它也存储着与其相关联的文档的指针。

● 框架窗口存储着指向当前活动视图的指针。

● 文档模板对象存储着打开文档的列表。

● 应用对象(唯一的全局对象)存储着文档模板的列表。

在对象创建时就建立了对象间的关系,了解这种关系后,就可以很方便地从给定的对象中访问其他的对象,具体如表1-5所示。

表1-5 各对象之间的访问

另外,调用全局函数AfxGetApp可以得到CWinApp应用类指针,而通过AfxGetApp()->m_pMainWnd则可以获得框架窗口指针。

1.11.2 MFC ODBC数据库开发技术

VisualC++中常用的数据库开发技术,大致分为3类,即ODBC API/MFC ODBC、DAO和ADO。其中,MFC ODBC是MFC对ODBC API的封装,ADO是OLE DB的高层接口。

本系统开发采用的是MFC ODBC技术。一个基于ODBC的应用程序对数据库的操作不依赖任何DBMS,不直接与DBMS打交道,所有的数据库操作由对应的DBMS的ODBC驱动程序完成。也就是说,不论是FoxPro、Access还是Oracle数据库,均可用ODBC API进行访问。由此可见,ODBC的最大优点是能以统一的方式处理所有的数据库。

ODBC包含的部件及各部件的关系,如图1-30所示。

图1-30 ODBC部件关系图

应用程序要访问一个数据库,首先必须用ODBC管理器注册一个数据源,管理器根据数据源提供的数据库位置、数据库类型及ODBC驱动程序等信息,建立起ODBC与具体数据库的联系。这样,只要应用程序将数据源名提供给ODBC,ODBC就能建立起与相应数据库的连接。

在访问ODBC数据源时,需要ODBC驱动程序的支持。用Visual C++ 6.0安装程序可以安装SQL Server、Access、Paradox、dBase、FoxPro、Excel、Oracle和Microsoft Text等驱动程序。

MFC的ODBC类对较复杂的ODBC API进行了封装,提供了简化的调用接口,从而大大方便了数据库应用程序的开发。程序员不必了解ODBC API和SQL的具体细节,利用ODBC类即可完成对数据库的大部分操作。

MFC的ODBC类主要包括:

● CDatabase类:主要功能是建立与数据源的连接。

● CRecordset类:该类代表从数据源选择的一组记录(记录集),通过该类可对记录集中的记录进行滚动、修改、增加和删除等操作。

● CRecordView类:提供了一个表单视图与某个记录集直接相关联,利用对话框数据交换机制(DDX)在记录集与表单视图的控件之间传输数据。该类支持对记录的浏览和更新,在撤销时会自动关闭与之相联系的记录集。

● CFieldExchange类:支持记录字段数据交换(DFX),即记录集字段数据成员与相应的数据库的表的字段之间的数据交换。该类的功能与CDataExchange类的对话框数据交换功能类似。

● CDBException类:代表ODBC类产生的异常。

概括地讲,CDatabase针对某个数据库,它负责连接数据源;CRecordset针对数据源中的记录集,它负责对记录的操作;CRecordView负责界面,而CFieldExchange负责CRecordset与数据源的数据交换,本系统只用到了CRecordset类。使用MFC ODBC类可以快速地开发简单的数据库应用程序,但缺乏灵活性。

1.11.3 VC窗口分隔技术

本系统窗口设计采用了静态窗口分隔技术。静态分隔窗口是指在窗口创建时,分隔窗口的窗格就已经创建好了,且窗格的数量和顺序不会改变,窗格为一个分隔条所分隔,用户可以拖动分隔条调整相应的窗格的大小。

CSplitterWnd类主要为窗口分隔提供了封装,窗口被分成各个窗格后,由该类的对象负责管理。对Windows来说,CSplitterWnd是一个真正的窗口,它完全占据了框架窗口的用户区域,而视窗则占据了分隔窗口的窗片区域,CSplitterWnd窗口不参与命令传递机制。

使用时,CSplitterWnd对象通常为其父框架窗口CFrameWnd或CMDIChildWnd(MDI应用中)对象的内嵌成员,CSplitterWnd对象的创建过程如下。

(1)在父框架窗口中嵌入CSplitterWnd对象成员。

(2)重载父框架窗口的CFrameWnd::OnCreateClient成员函数。

(3)从上一步重载的函数内部调用Create创建动态分隔窗口或者调用CreateStatic创建静态分隔窗口。

下面介绍一下CSplitterWnd类中的几个常用的函数。

1. 创建动态分隔窗口函数Create

该函数用来创建动态分隔窗口,同时将该窗口与类CSplitterWnd相关联,其声明如下。

    BOOL Create( CWnd* pParentWnd, int nMaxRows, int nMaxCols, SIZE sizeMin,
    CCreateContext* pContext, DWORD dwStyle = WS_CHILD | WS_VISIBLE |WS_HSCROLL |
    WS_VSCROLL | SPLS_DYNAMIC_SPLIT, UINT nID = AFX_IDW_PANE_FIRST );

各主要参数的含义如下。

● pParentWnd:分隔窗口的父框架窗口指针。

● nMaxRows和nMaxCols:行与列的最大值,二者均不大于2。

● sizeMin:指定窗格被显示时的最小值,例如拖动分隔框的幅度小于相应的值时,窗格将不会被显示,但是可以通过调用该类的另外两个成员对此值进行改变。

动态分隔窗口的典型代码可表示如下。

    BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
    {
      return m_wndSplitter.Create(this,
          2,2,                                       //设置行与列的数目
          CSize(10,10),                                //窗格被显示时的最小值
          pContext);
    }

2. 创建静态分隔窗口函数CreateStatic

该函数用来创建静态分隔窗口,同时将该窗口与类CSplitterWnd相关联,其声明如下。

    BOOL CreateStatic( CWnd* pParentWnd, int nRows, int nCols, DWORD dwStyle =
                WS_CHILD | WS_VISIBLE, UINT nID = AFX_IDW_PANE_FIRST );

它的主要参数的含义如下。

● pParentWnd:分隔窗口的父框架窗口指针。

● nMaxRows和nMaxCols:行与列的最大值,二者均不大于16。

● dwStyle:窗口风格,默认为子窗口且可见,如果添加滚动条,则需要另外设置WS_HSCROLL和WS_VSCROLL。

3. 创建窗格视图函数CreateView

CreateView函数为静态分隔窗口创建窗格视图。在框架显示分隔的窗口之前,静态分隔窗口的所有窗格都必须创建完毕,它的声明如下。

    virtual BOOL CreateView( int row, int col, CRuntimeClass* pViewClass, SIZE sizeInit,
                        CCreateContext* pContext );

各主要参数的含义如下。

● row:分隔窗口中,新建视图所在的行。

● col:分隔窗口中,新建视图所在的列。

● pViewClass:新建视图的CRuntimeClass的指针。

● sizeInit:新建视图的初始化大小。

创建静态分隔窗口,并指定各窗格的视图的典型代码如下。

    BOOL CMainFrame::OnCreateClient( LPCREATESTRUCT lpcs, CCreateContext* pContext)
    {
      BOOL bCreateSpltr=m_wndSplitter.CreateStatic(this,2,1);            //静态分隔窗口
      //为两个窗格指定视图
      m_wndSplitter.CreateView(0,0,RUNTIME_CLASS(COneView), CSize(0,0), pContext);
    m_wndSplitter.CreateView(1,0,RUNTIME_CLASS(CAnotherView), CSize(0,0), pContext);
      return (bCreateSpltr);
    }

另外,CSplitterWnd还提供了一些用于获取和设置窗格属性的函数,常用的函数及其功能如表1-6所示。

表1-6 CSplitterWnd类中常用的函数及其功能