Глава 21.
Настройка приложения на работу с базами данных
Старость наступает тогда, когда вам уже ничего не хочется пробовать.
Борис Васильев
В главе 20 мы уже создали каркас простейшего приложения для того, чтобы можно было манипулировать информацией из базы данных Northwind, а также получили необходимую информацию о классах библиотеки MFC, предназначенных для доступа к базам данных на основе стандарта ODBC. Теперь, имея все это под рукой, можно приступать к добавлению необходимого кода для того, чтобы извлекать, обновлять, добавлять, удалять и отображать для пользователя информацию из базы данных. К этому мы и переходим.
Извлечение информации из базы данных
Прежде чем рассматривать вопрос "Каким образом можно извлечь информацию из базы данных?", необходимо выполнить некоторые подготовительные действия, чтобы можно было видеть на экране запрашиваемую информацию. Поэтому сначала познакомимся с методами отображения данных.
Подготовка формы для отображения данных
В нашем простейшем примере мы должны научиться манипулировать данными одной единственной таблицы — Products. Используемые нами столбцы этой таблицы перечислены в табл. 21.1.
Таблица 21.1. Используемые столбцы таблицы Products
Столбец |
Назначение |
ProductID |
Числовой уникальный идентификатор, автоматически присваиваемый новому продукту (тип Autonumber) |
ProductName |
Текстовая строка, содержащая название продукта |
QuantityPerUnit |
Текстовая строка, характеризующая содержимое партии продукта |
UnitPrice |
Цена партии; денежный тип данных |
Теперь нам необходимо подготовить форму, которая будет отображать требуемую информацию. Примерный вид такой формы представлен на рис. 21.1.
Рис. 21.1. Нужно создать форму для отображения информации о продуктах
Примечание
Если вы хотите, как и я, использовать русские названия для полей (столбцов) таблицы, то перед тем, как набирать эти названия, необходимо изменить основной язык для окна диалога. Сделать это очень просто — щелкните правой кнопкой мыши на идентификаторе окна диалога IDD_DB_FORM (на рис. 21.1 он обведен рамкой) и в появившемся контекстном меню выберите команду Properties (Свойства), в результате чего на экране появится диалоговое окно Dialog Properties (Свойства диалога) (рис. 21.2). В комбинированном списке Language (Язык) выберите Russian.
Если теперь скомпилировать и запустить приложение, то на экране появится окно, аналогичное представленному на рис. 21.3.
Рис. 21.2. Диалоговое окно Dialog Properties
Рис. 21.3. Приложение DB отображает форму с пустыми полями
Здесь вы должны обратить внимание на две проблемы: первая — внешний вид формы оставляет желать лучшего и вторая — несмотря на то, что мы присвоили всем элементам управления формы соответствующие идентификаторы и знаем, что в таблице базы данных есть информация, она не отображается в окне.
И если в этот момент вы подумали, что первая проблема не имеет никакого отношения к рассматриваемому вопросу, то вы глубоко заблуждаетесь: даже если ваше приложение будет работать безукоризненно, мало кто, кроме вас, захочет им воспользоваться — дизайн пользовательского интерфейса имеет одно из основных значений при разработке любого программного продукта.
Пока я писал, а вы читали эти строки, можно было бы уже придать окну более привлекательный вид, настолько это просто: необходимо только немного модифицировать функцию CMainFrame::PreCreateWindow. Вот как она должна выглядеть (здесь и далее полужирным шрифтом выделен код, добавленный мною):
BOOL CMainFrame::PreCreateWindowfCREATESTRUCTS cs)
{
// Изменяем размеры главного окна
cs.cx = 376;
cs.cy = 272;
return CFrameWnd::PreCreateWindow(cs);
}
Примечание
Ваши значения могут отличаться от приведенных здесь, поскольку они зависят от размеров окна диалога. У меня они равны 242x106, и в качестве шрифта установлен MS Sans Serif, 8, что, как вы помните, имеет существенное значение.
В результате этих минимальных добавлений внешний вид окна формы существенно улучшился (рис. 21.4).
Рис. 21.4. Теперь форма выглядит значительно лучше
После усовершенствования внешнего вида формы переходим непосредственно к работе с базой данных.
Отображение и обновление содержимого базы данных
Для того чтобы отобразить данные из таблицы базы данных, необходимо выполнить определенные действия, о которых вы узнали, читая главу 20. Перечислю их здесь еще раз в сжатой форме. Для получения информации из таблицы базы данных требуется:
Мы пока ничего из этого не сделали. Правда, очень и очень многое за нас выполнила библиотека MFC, но это не избавляет нас от необходимости четко понимать все этапы процедуры.
Посмотрите еще раз на рис. 21.4, а лучше запустите приложение DB, обратив внимание на то, что на панели инструментов кнопки перемещения на следующую и последнюю записи доступны, а на предыдущую и первую — нет. Попробуйте понажимать на них. Как видите, они реагируют на нажатия, хотя в полях ничего и не отражается. О чем это говорит? О том, что библиотека MFC сама реализовала три первых этапа процесса получения информации, воспользовавшись для этого теми параметрами, которые мы указали при создании проекта.
CString CDBSet::GetDefaultConnect()
{
// Параметры источника данных
return _T("ODBC;DSN=MS Access 97 Database");
}
CString CDBSet::GetDefaultSQL()
{
// Параметры таблицы
return _T("[Products]");
}
class CDBSet : public CRecordset
{
public:
// Переменные для отображения данных строки таблицы
//{{AFX_FIELD(CDBSet, CRecordset)
long m_ProductID;
CString m_ProductName;
long m_CategoryID;
CString m_QuantityPerUnit;
CString mJJnitPrice;
//}}AFX_FIELD
...
}
На нашу долю осталась реализация только последнего, четвертого, этапа — отображение результатов. Сделать это совсем просто— нужно только создать соответствующие переменные. Раскройте спроектированное ранее диалоговое окно IDD_DB_FORM и при нажатой клавише <Ctrl> дважды щелкните на элементе управления. В результате на экране появится диалоговое окно Add Member Variable (Добавить компонентную переменную), показанное на рис. 21.5. В комбинированном списке Member variable name (Имя компонентной переменной) этого окна необходимо выбрать соответствующую переменную — в данном случае m_pSet->m_ProductName.
Рис. 21.5. Диалоговое окно Add Member Variable
Примечание
На мой взгляд, данный пример идеально иллюстрирует положение, согласно которому всем идентификаторам, переменным и т. д. необходимо давать содержательные имена. Например, если бы мы для элемента управления IDC_NAME использовали имя IDC_SOMETHING и нечто подобное для других элементов управления, то было бы трудно выбрать правильную переменную.
Здесь m_pSet — указатель на объект класса, производного от CRecordset, содержащий текущую запись. Переменная m_ProductName в этом объекте содержит текущее значение поля ProductName из таблицы Products. Если вы посмотрите содержимое комбинированного списка Member variable name, то увидите указатель для всех полей запроса, который для нас сконструировала библиотека MFC, и поэтому можно повторить рассмотренную процедуру сопоставления элементов управления формы с переменными для каждой из них.
После того как все элементы управления сопоставлены переменным, обязательно убедитесь, что все сделано корректно. Используйте для этого команду View/Class Wizard... (или одноименную кнопку на панели инструментов), чтобы отобразить диалоговое окно MFC ClassWizard, в котором выберите вкладку Member Variables (Компонентные переменные). В раскрывающемся списке Class name необходимо выбрать класс CDBView. Список компонентных переменных должен выглядеть так, как показано на рис. 21.6.
Рис. 21.6. Компонентные переменные класса CDBView
Теперь скомпилируйте и выполните приложение. Сразу после запуска вы увидите окно, показанное на рис. 21.7. Попробуйте перемешаться по записям базы данных, воспользовавшись соответствующими кнопками на панели инструментов или командами меню. И хотя мы пока не написали ни строчки кода, приложение работает прекрасно — можно переходить от записи к записи и даже изменять значения полей. Внесем некоторые изменения в приложение, чтобы добавить к значению цены обозначение денежной единицы.
Рис. 21.7. Результат запуска приложения DB
Но перед этим необходимо внести некоторые изменения, связанные с компонентными переменными класса. Используйте команду View/Class Wizard. чтобы отобразить диалоговое окно мастера MFC ClassWizard. Выберите вкладку Member Variables и класс CDBView аналогично тому, как вы это делали раньше. Первое, что необходимо выполнить — это удалить переменную, обозначенную в настоящий момент как IDC_PRICE. Выделите ее и нажмите кнопку Delete Variable. Теперь нажмите на кнопку Add Variable... и вы увидите диалоговое окно Add Member Variable. Наберите m_cPrice в качестве имени компонентной переменной (Visual C++ подставляет т_ автоматически). Нажмите кнопку ОК, чтобы добавить переменную. Диалоговое окно MFC ClassWizard должно выглядеть аналогично показанному на рис. 21.8.
Рис. 21.8. Диалоговое окно MFC ClassWizard после внесенных изменений
Теперь, когда требуемая переменная создана, можно дополнить приложение необходимым кодом. Конечно, первое, что нам требуется — это отобразить информацию базы данных так, чтобы позднее мы могли видеть изменения. Ниже приведен фрагмент кода, который следует добавить в функцию DoDataExchangeQ класса CDBView. Убедитесь, что вы вставили новые строчки в начало, а не в конец функции, т. к. в противном случае получите странные результаты.
void CDBView::DoDataExchange(CDataExchange* pDX)
{
int nLen, dec;
CString tmp = "";
// Получаем значение цены из результирующего набора
m_Price = m_pSet->m_UnitPrice;
// Получаем длину строки ...
nLen = m_Price.GetLength();
//и положение десятичной точки
dec = m_Price.Find('.');
// Удаляем заключительные нули и добавляем в начало '$'
if((dec + 3) <= nLen)
tmp = m_Price.Left(dec + 3) ;
if (Itmp.IsEmpty() )
m_Price = "$";
// Записываем результат в специальную переменную
m_Price += tmp;
// Обмен данными с результирующим набором,
//на который указывает
m_pSet CRecordView::DoDataExchange(pDX);
//{(AFX_DATA_MAP(CDBView)
DDX_FieldText(pDX, IDC_NAME, m_pSet->m_ProductName, m_pSet);
DDX_FieldText(pDX, IDC_PRODID, m_pSet->m_ProductID, mjpSet);
DDX_FieldText(pDX, IDCJJNIT, m_pSet->m_QuantityPerUnit, mjpSet);
DDX_Text(pDX, IDC_PRICE, m_Price);
//}}AFX_DATA_MAP
}
Скомпилируйте и запустите приложение. Результат представлен на рис. 21.9.
Рис. 21.9. Результат работы приложения DB после внесенных изменений
Как видите, все работает. Однако теперь возникла другая проблема — при попытке изменения поля "Цена" мы получаем странные результаты. Для устранения этой проблемы добавим некоторый код в конец функции DoDataExchangeQ класса CDBView, чтобы обновить базу данных после того, как изменили данные с помощью элемента управления в форме. Окончательный, на данный момент, вид функции DoDataExchangeQ приведен ниже.
void CDBView::DoDataExchange(CDataExchange* pDX)
{
int nLen, dec;
CString tmp = "";
// Получаем значение цены из результирующего набора
m_Price = m_pSet->m_UnitPrice;
// Получаем длину строки ...
nLen = m_Price.GetLength();
// и положение десятичной точки.
dec = m_Price.Find('.');
// Удаляем заключительные нули и добавляем в начало '$'
if((dec + 3) <= nLen)
tmp = m_Price.Left(dec + 3); if(!tmp.IsEmpty())
m_Price = "$";
// Записываем результат в специальную переменную m_strPrice += tmp;
// Обмен данными с результирующим набором,
//на который указывает
m_pSet CRecordView::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CDBView)
DDX_FieldText(pDX, IDC_NAME, m_pSet->m_ProductName, m_pSet);
DDX_FieldText(pDX, IDC_PRODID, m_pSet->m_ProductID, m_pSet);
DDX_FieldText(pDX, IDC_UNIT, mjpSet->m_QuantityPerUnit, m_pSet);
DDX_Text(pDX, IDC_PRICE, m_Price);
//}}AFX_DATA_MAP
// Получаем длину отроки
nLen = m_Price.GetLength();
// Удаляем знак '$', если он есть
if(m_Price.Find('$') != -1)
m_pSet->m_OnitPrice = m_Prioe.Right(nLen — 1);
else
m_pSet->m_UnitPrice = m_Prioe;
}
Если сейчас выполнить приложение, то можно будет перемещаться по записям базы данных, а также их корректировать. Кому-то может показаться, что для этого потребовалось достаточно много времени. Но уверяю вас, что это только в начале — по мере того, как вы будете разрабатывать новые и новые приложения, необходимое для этого время будет в основном тратиться на разработку логики работы, а отнюдь не на реализацию кода.
Добавление и удаление записей в таблице
Всем понятно, что созданный только что пример не имеет практического значения — мало кому нужна программа, которая позволяет только просматривать, пусть даже и обновлять, данные в одной таблице. Поэтому необходимо продолжить работу над приложением и добавить возможность добавления и удаления записей. Этим мы сейчас и займемся.
Как и многое другое, эту возможность реализовать достаточно просто, поскольку большую часть работы мы можем переложить на библиотеку MFC. Основная наша задача — это корректным образом предоставлять ей информацию.
Как обычно, сначала позаботимся о некоторых связанных с основной задачей вопросах. Прежде всего необходимо создать либо элемент меню, либо кнопку на панели инструментов, либо и то, и другое. Чтобы не перегружать текст и основываясь на собственных симпатиях, остановимся на создание кнопки.
1. Раскройте вкладку Resource View (ресурсы) окна проектов. Щелкните по значку +, расположенному рядом с папкой Toolbar, а затем по IDR_MAINFRAME. Появится панель инструментов.
2. Выделите пустую кнопку, расположенную справа, и нарисуйте какой-либо символ New Record (Новая запись), например, такой, как показан на рис. 21.10. Переместите кнопку на место, расположенное сразу после четырех кнопок перемещения по базе данных.
Рис. 21.10. Новая кнопка на панели инструментов для добавления новой записи
3. Дважды щелкните по вновь созданной кнопке на разрабатываемой панели инструментов. Раскроется диалоговое окно Toolbar Button Properties , (Свойства кнопки панели инструментов). Введите в качестве идентификатора в список ID (Идентификатор) ID_RECORD_APPEND.
4. В поле Prompt (Подсказка) наберите текст — Append a new record to the table.\nNew Record (Добавление новой записи в таблицу\nНовая запись), как показано на рис. 21.11.
5. Нажмите кнопку закрытия в правом верхнем углу диалогового окна Toolbar Button Properties, и Visual C++ автоматически присвоит новой кнопке идентификатор ID_RECORD_APPEND.
Рис. 21.11. Диалоговое окно свойств кнопок панели инструментов
Теперь необходимо сопоставить созданной кнопке некоторые действия. Откройте окно Class Wizard, выполнив команду View/ClassWizard, выберите вкладку Message Maps (карта сообщений) и в списке Object IDs выделите элемент ID_RECORD_APPEND, а в раскрывающемся списке Class name выберите CDBView — класс, который служит для просмотра базы данных. Именно для него потребуется добавить некоторый код.
Далее в списке Message выделите элемент COMMAND и нажмите кнопку Add Function, в результате чего на экране появится диалоговое окно Add Member Function, в котором ,уже предложено некоторое имя (рис. 21.12), базирующееся на идентификаторе кнопки (присваивайте всем идентификаторам содержательные имена!).
Рис. 21.12. Если идентификатору присвоено содержательное имя, то можно просто нажать кнопку ОК
Нажмите кнопку ОК, и диалоговое окно MFC ClassWizard примет вид, показанный на рис. 21.13. В этом окне нажмите кнопку Edit Code (Редактировать код), чтобы сразу перейти к шаблону созданной функции, куда потребуется добавить код, приведенный ниже.
void CDBView::OnRecordAppend()
{
// Сначала проверяем, не была ли открыта база данных
// в режиме только для чтения. Если это так,
//то выводим предупреждение и выходим из функции
if(m_pSet->CanAppend() == 0)
MessageBox("Можно только просматривать записи",
"Ошибка добавления записи",
МВ_ОК | MB_ICONERROR);
else
{
// Перемещаемся на первую запись
m_pSet->MoveFirst();
// Создаем пустую запись,
// в которую пользователь будет вводить значения
m_pSet->AddNew();
// Устанавливаем флаг перехода в режим добавления записи
m_bAdd = TRUE;
// Обновляем поля формы
UpdateData(FALSE);
}
}
Рис. 21.13. Диалоговое окно MFC ClassWizard после проведенных действий
Рассмотрим, как работает функция OnRecordAppend. Прежде всего она проверяет, можно ли добавлять данные в базу данных. Сделать это можно с помощью функции CanAppend класса CRecordset, которая была подробно описана в главе 20. Если добавлять записи нельзя, то необходимо вывести сообщение об ошибке и завершить работу, что и сделано в нашем примере.
Далее нужно подготовить элементы управления в форме для ввода новых значений. Для этого мы перемещаемся в начало набора записей (функция MoveFirst), вызываем специальную функцию AddNew, подготавливающую результирующий набор к приему новой записи, и устанавливаем флаг, что это новые данные. Осталось только очистить поля элементов управления, что и делает функция UpdateData(FALSE).
Итак, элементы управления в форме и результирующий набор готовы к приему данных. Теперь необходимо решить вопрос, каким образом и где передать введенные в форме значения в результирующий набор. Самым простым представляется способ добавления специальной кнопки, нажатие на которую вызовет специальный обработчик, где введенные значения перепишутся в результирующий набор. Второй способ не требует создания дополнительных элементов управления и для фиксации значений использует переопределенную функцию перемещения по записям результирующего набора Move (как получить для нее заготовку, вы уже должны знать).
BOOL CDBView::OnMove(UINT nlDMoveCommand)
{
// Проверяем, в каком режиме — добавления или перемещения —
// мы находимся
if(m_bAdd)
{
// Читаем данные из элементов управления в форме
// непосредственно в результирующий набор
if(!UpdateData())
return FALSE;
// Переписываем новые данные в базу данных,
// позаботившись об обработке ошибок посредством механизма
// обработки исключений
try
{
m_pSet->Update() ;
}
catch(CDBException e)
{
AfxMessageBox(e.m_strError);
return FALSE
}
// Поскольку используется результирующий набор типа
// "мгновенный список", перечитываем информацию из базы данных
m_pSet->Requery();
// Обновляем поля формы
UpdateData(FALSE);
// "Выходим" из режима добавления
m_bAdd = FALSE;
}
else
// Переходим к следующей записи в результирующем наборе
return CRecordView::OnMove(nIDMoveCommand);
}
Кроме того, необходимо определить переменную m_bAdd:
class CDBView : public CRecordView
{
private:
BOOL m_bAdd;
};
CDBView::CDBView() : CRecordView(CDBView::IDD)
{
m_bAdd = FALSE;
}
Рассмотрим логику работы функции OnMove. Если мы попали в нее после ввода новой записи (установлен флаг m_bAdd), то переписываем введенные значения в базу данных. В противном случае просто переходим к следующей записи. Нас, естественно, интересует режим добавления. Прежде всего необходимо прочитать данные из элементов управления формы. Обратите внимание, что переменные класса CDBView определены так, что мы это делаем непосредственно в результирующий набор, используя функции обмена полями данных, например, для поля IDC_PROD.
DDX_FieldText(pDX, IDC_PROD, m_pSet->m_ProductName, m_pSet);
Для отслеживания возможных ошибок мы воспользовались механизмом обработки исключений, реализованном в библиотеке MFC.
Примечание
Для тех, кто заинтересуется этим механизмом, могу порекомендовать книгу А. Мешкова и Ю. Тихомирова "Visual C++ и MFC", в которой данный вопрос изложен достаточно подробно.
Если никакой ошибки не произошло, то перечитаем информацию из базы данных, чтобы работать с самым последним набором. Теперь осталось только обновить элементы управления в форме. Помните, в функции OnRecordAppend мы перед входом в режим добавления переместились на первую запись результирующего набора. Поэтому необходимо вызвать функцию UpdateData(FALSE), чтобы отобразить ее в элементах управления формы. И, наконец, не забываем сбросить флаг m_bAdd, чтобы избежать ошибок.
Вернемся к табл. 21.1, где представлены поля таблицы Products. Видим, что у столбца ProductID установлен тип данных AutoNumber. Это означает, что система управления базами данных (в нашем случае Access) сама позаботится о присвоении значения этому столбцу. Попробуйте ввести какое-либо значение, меньшее 77, в поле "Идентификатор". Результат, который вы получите, представлен на рис. 21.14.
Рис. 21.14. Попытка вставить повторяющееся значение в столбец, для которого установлен тип AutoNumber
Чтобы решить эту проблему, достаточно просто поменять переменную, с которой связан элемент управления IDC_PRODID, и тем самым переложить задачу на Access. Вот как это должно выглядеть (рис. 21.15).
Рис. 21.15. Для элемента управления IDC_PRODID необходимо определить другую переменную
Кроме этого, в код требуется также добавить одну строку, чтобы можно было видеть информацию об идентификаторе.
void CDBView::DoDataExchange(CDataExchange* pDX)
{
// Для отображения значений поля ProductID таблицы
// необходимо выполнить присваивание
m_ProdID = m_pSet->m_ProductID ;
// Осуществляем обмен данными с элементами управления в форме
CRecordView::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CDBView)
DDXJText(pDX, IDC_PRICE, m_Price);
DDX_FieldText(pDX, IDC_NAME, m_pSet->m_ProductName, m_pSet);
DDX_FieldText(pDX, IDC_ONIT, m_pSet->m_QuantityPerUnit, m_pSet);
DDXJText(pDX, IDCJPRODID, m_ProdID);
//}}AFX_DATA_MAP
}
Вот, собственно, и все, что надо выполнить. Однако, чтобы не возникало искушения вводить что-либо в поле идентификатора IDC_PRODID, уберем его с экрана на время вставки новой записи. Для этого немного модифицируем код в двух функциях OnRecordAppend и On Move, как показано ниже.
void CDBView::OnRecordAppend()
{
// Сначала проверяем, не была ли открыта база данных
// в режиме только для чтения. Если это так,
//то выводим предупреждение и выходим из функции
if(m_pSet->CanAppend() = 0)
MessageBox.("Можно только просматривать записи",
"Ошибка добавления записи",
МВ_ОК | MB_ICONERROR);
else
{
// Скрываем элементы управления на время ввода новой записи
GetDlgItem(IDC_PRODST)->ShoWWindow(SWJHIDE);
GetDlgItem(IDC_PRODID)->ShowWindow(SW_HIDE);
// Устанавливаем фокус на поле ввода названия
GetDlgltem(IDC_NRME)->SetFoous();
// Перемещаемся на первую запись
m_pSet->MoveFirst () ;
// Создаем пустую запись,
//в которую пользователь будет вводить значения
m_pSet->AddNew();
// Устанавливаем флаг перехода в режим добавления записи
m_bAdd = TRUE;
// Обновляем поля формы
UpdateData(FALSE);
}
}
BOOL CDBView::OnMove(UINT nIDMoveComnand)
{
// Проверяем, в каком режиме — добавления или перемещения -
// мы находимся
if(m_bAdd)
{
// Читаем данные из элементов управления в форме
// непосредственно в результирующий набор
if(!UpdateData())
return FALSE;
// Переписываем новые данные в базу данных,
// позаботившись об обработке ошибок посредством механизма
// обработки исключений
try
{
m_pSet->Update();
}
catch(CDBException e)
{
AfxMessageBox(e.m_strError);
return FALSE;
}
// Поскольку используется результирующий набор типа
// "мгновенный список", перечитываем информацию из базы данных
m_pSet->Requery();
// Обновляем поля формы
UpdateData(FALSE);
// "Выходим" из режима добавления
m_bAdd = FALSE;
// После завершения ввода снова отображаем элементы управления
GetDlgItem(IDC_PRODST)->ShowWindow(SW_SHOW);
GetDlgltem(IDC_PRODID)->ShowWindow(SW_SHOW);
}
else
// Переходим к следующей записи в результирующем наборе
return CRecordView::OnMove(nlDMoveCommand);
}
После внесения этих изменений скомпилируйте и запустите приложение. После нажатия кнопки New Record и ввода информации о новом продукте основное окно будет иметь вид, аналогичный представленному на рис. 21.16.
Рис. 21.16. Так теперь выглядит основное окно приложения в режиме ввода новой записи в базу данных
Примечание
Не забудьте изменить идентификатор статического поля "Идентификатор" на IUU_PKODST, иначе во время компиляции получите сообщение об ошибке
Итак, теперь мы умеем обновлять и добавлять записи в базу данных Следующим шагом является удаление записей.
Как обычно, начнем с того, что добавим в наше приложение кнопку на панели инструментов, нажатие на которую позволит перейти в режим удаления Чтобы не придумывать чего-либо непонятного, воспользуемся стандартным способом удаления, принятым во всех приложениях Windows (рис. 21.17). Создайте функцию OnRecordDelete, воспользовавшись стандартной описанной ранее, процедурой добавления новой функции, результат вызова кототрой представлен на рис. 21.18:
void CDBView::OnRecordDelete()
{
// Последняя возможность сохранить запись
if(MessageBoxC-Вы действительно хотите удалить эту запись?",
"Удаление записи",
MB_OKCANCEL|MB_ICONQUESTION| MB_DEFBUTTON2) = = IDOK)
// Удаляем запись
try
{
m_pSet->Delete();
}
catch(CDBException e)
{
// Если что-то произошло — сообщаем об этом
AfxMessageBox(e.m_strError);
return;
}
// Поскольку используется результирующий набор типа
// "мгновенный список", перечитываем информацию из базы данных.
m_pSet->Requery();
// После перечитывания данных из базы данных
// переходим к следующей записи m_pSet->MoveNext();
// Если удаляем запись из конца,
//то устанавливаем указатель на последнюю запись
if(m_pSet->IsEOF())
m_pSet->MoveLast ();
// Если таблица стала пустой, то очищаем поля
if(m_pSet->IsBOF ())
m_pSet->SetFieldNull(NULL);
}
// Обновляем поля формы
UpdateData(FALSE);
}
Рис. 21.17. Добавляем на панель инструментов новую кнопку
Рис. 21.18. После отказа от продукта запись о нем нужно удалить из базы данных
Как видите, принцип здесь тот же, что и при добавлении записи. Правила хорошего тона требуют, чтобы перед удалением пользователю был задан вопрос, хорошо ли он подумал и не случайно ли нажал кнопку удаления. Если запись действительно надо удалить, то смело нажимайте кнопку ОК, после чего будет вызвана функция Delete класса CRecordset, которая проведет корректное удаление записи из базы данных. Чтобы обновить набор, с которым мы работаем, необходимо вызвать функцию Requery, иначе мы будем продолжать работать с записью, которой в базе данных уже нет. Этот шаг был бы не нужен, если бы мы работали с динамическим набором (dynaset) — как вы помните, в этом случае результирующий набор обновляется одновременно с обновлением базы данных. Новыми при удалении являются следующие два действия — проверка на выход за начало и конец записей. Действительно, если мы переместились (вызов функции MoveNext) за последнюю запись (проверка if (m_pSet->isEOF ())), то непонятно, на что в этом случае будет указывать указатель. Поэтому его нужно установить на последнюю запись результирующего набора (вызов m_pSet->MoveLast ()). Аналогичные действия необходимо предпринять для проверки на выход за "левую границу", т. е. за первую запись (вызов if (m_pSet->isBOF())). Эта ситуация возникает тогда, когда из таблицы удаляется последняя строка и требуется очистить поля единственной пустой строки (ВЫЗОВ m_pSet->SetFieldNull (NOLL) ).
Как и в случае обновления и добавления новой записи, здесь также всю основную работу делает библиотека MFC, а точнее, ее класс CRecordset.
Но пойдем дальше и рассмотрим еще некоторые возможности работы с базой данных.
Используя классы библиотеки MFC, организовать сортировку записей очень просто. Если обратиться к главе 20, где описаны компоненты классов, то ответ на вопрос: "Как организовать сортировку записей таблицы базы данных?" становится тривиальным. Поэтому перейду сразу к примеру. Для демонстрации этой возможности создадим специальные элементы меню, внешний вид которых представлен на рис. 21.19.
Рис. 21.19. Элементы меню, позволяющие задавать различные критерии сортировки
Как видите, мы использовали для организации сортировки три различных критерия: по идентификатору продукта, по названию и цене. После того как элементы меню созданы и им присвоены соответствующие идентификаторы (пример одного такого идентификатора приведен на рис. 21.20), необходимо создать обработчик соответствующих команд.
Рис. 21.20. Окно свойств элементов меню
Для этого можно воспользоваться ClassWizard, но тогда придется для каждой команды создавать свой собственный обработчик, а можно использовать специальный тип обработчика, который позволяет "реагировать" на команды из некоторого диапазона. По этому пути мы и пойдем. Для облегчения работы начнем все-таки с помощью ClassWizard и создадим обработчик для какой-нибудь одной команды, например, ID_BY_ID. Способ, которым это делается, ничем не отличается от рассмотренного нами для обработчиков команд кнопок панели инструментов. Поскольку в дальнейшем мы будем модифицировать этот обработчик, чтобы он "мог работать" со всеми командами сортировки, дадим ему обобщенное имя — OnSort.
Теперь необходимо внести некоторые изменения в код, созданный ClassWizard, согласно приведенному ниже тексту:
BEGIN_MESSAGE_MAP(CDBView, CRecordView)
//{{AFX_MSG_MAP(CDBView)
ON_COMMAND(ID_RECORD_APPEND, OnRecordAppend)
ON_COMMAND(ID_RECORD_DELETE, OnRecordDelete)
ON_COMMftND_RRNGE(ID_SORT_ID, ID_SORT_PRICE, OnSort)
ON_UPDATE_COMMAND_UI_RANGE(ID_SORT_ID, ID_SORT_PRICE,
OnUpdateSort)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
class CDBView : public CRecordView
{
.// Generated message map functions
protected:
// Переменная, позволяющая отслеживать текущий режим сортировки
UINT m_nSort;
};
Теперь необходимо добавить код в обработчики сообщений:
// Обработчик команд ID_SORT_ID — ID_S'ORT_PRICE (один на все)
void CDBView::OnSort()
{
// Получаем идентификатор выбранной команды
m_nSort = LOWORD(GetCurrentMessage()->wParam);
// Организуем установку соответствующего критерия сортировки
switch(mjnSort)
}
// Сортировка по идентификатору
case ID_SORT_ID:
m_pSet->m_strSort = "ProductID";
break;
// Сортировка по названию
case ID_SORT_TITLE:
m_pSet->m_strSort = "ProductName";
break;
// Сортировка по цене
case ID_SORT_PRICE:
m_pSet->m_strSort = "UnitPrice";
break;,
}
// Чтобы учесть новый порядок сортировки,
// необходимо выполнить запрос к базе данных
m_pSet->Requery();
// Перемещаемся к первой записи результирующего набора
m_pSet->MoveFirst();
// Обновляем содержимое элементов управления в форме
UpdateData(FALSE);
}
// Обработчик команд обновления
// ID_SORT_ID - ID_SORT_PRICE (один на все)
void CDBView::OnUpdateSort(CCmdUI* pCmdUI)
{
// Устанавливаем или убираем галочку
//у соответствующего элемента меню
pCmdUI->SetCheck(pCmdUI->m_nID= m_nSort);
}
Все на удивление просто, и приведенных комментариев должно быть вполне достаточно, чтобы разобраться с тем, каким образом реализуются различные режимы сортировки. Скомпилируйте и запустите приложение. Результат его работы при выборе сортировки по названию представлен на рис. 21.21.
Рис. 21.21. Продукты отсортированы по названию
Если вы решите обойтись без помощи ClassWiz;ard, то не забудьте также добавить необходимые строки в определение класса CDBView.
class CDBView : public CRecordView
{
// Generated message map functions
protected:
// Переменная, позволяющая отслеживать текущий режим сортировки
UINT m_nSort;
//{{AFX_MSG(CDBView)
afx_msg void OnRecordAppend();
afx_msg void OnRecordDelete();
afx msg void OnSortf);
afx_msg void OnUpdateSort(CCmdUI* pCmdUI);
AFX_MSG
Последний режим, который мы рассмотрим в этой главе, относится к работе с компонентной переменной m_srtFilter класса CRecordset.
Поиск информации в базе данных
Вопросам организации наиболее эффективного поиска посвящены целые книги. Поэтому выбору наиболее эффективного режима поиска мы не будем уделять внимание. При использовании ODBC существует простой способ, который, может быть, не является самым быстрым и эффективным, но в то же время является абсолютно надежным.
Очевидно, что, как обычно, необходимо потратить некоторое время на модификацию меню и/или панели инструментов, прежде чем переходить непосредственно к поиску. Я больше не буду останавливаться на этом вопросе и только приведу внешний вид кнопки, которую я добавил (рис. 21.22).
Рис. 21.22. Кнопка, которая выглядит подобно паре очков, предназначена для запуска поиска записи в базе данных
Для того чтобы запросить у пользователя, что именно нужно найти, необходимо создать небольшое диалоговое окно. Вернитесь в окно Resource View. Щелкните правой кнопкой мыши на папке Dialog и выберите из контекстного меню Insert Dialog (Вставить диалоговое окно). Visual C++ представит новое окно диалога. Необходимо изменить некоторые его свойства. Прежде всего нужно присвоить этому диалоговому окну более содержательное имя. Щелкните правой кнопкой мыши на элементе IDD_DIALOG1 в Resource View и выберите Properties из контекстного меню. Вы увидите диалоговое окно Dialog Properties, аналогичное показанному на рис. 21.23.
Рис. 21.23. Свойства блока диалога
Наберите IDD_DLG_FIND в поле ID. Далее следует дать этому диалоговому окну более содержательный заголовок. Наберите "Поиск информации в базе данных" в поле ввода Caption (Заголовок), а затем нажмите кнопку. Close, расположенную в правом верхнем углу окна, чтобы завершить, ввод изменений.
Теперь нам необходимо добавить один элемент управления Static Text и свя- занный с ним элемент управления Edit Box, чтобы завершить создание блока диалога. Его окончательный вид показан на рис! 21.24 ; Г'1,
Последняя часть работы для этого диалогового окна — установить свойства; элемента управления Edit Box. Щелкните правой кнопкой мыши на этом; элементе управления и выберите Properties из контекстного меню. Измените свойство ID в диалоговом окне Edit Properties (Свойства текстового поля) на IDC_RECORD_FIND. Нажмите кнопку Close, расположенную в правом верхнем углу окна. Теперь при нажатой клавише <Ctrl> дважды щелкните на элементе управления Edit Box. При этом вы увидите блок диалога Adding a Class (Добавить класс), аналогичный показанному на рис. 21.25.
Причина появления этого блока диалога заключается в том, что каждый диалог в вашей программе должен быть с чем-то связан. Так же, как окно About, имеющееся в любом приложении, каждое диалоговое окно должно иметь свой собственный класс. Нажмите кнопку ОК, чтобы создать новый класс для этого диалогового окна. Visual C++ отобразит диалоговое окно New Class, в котором нужно набрать имя класса в поле Name. Вы можете использовать любое имя класса, какое хотите, а я выбрал CFindPrice. Нажмите кнопку ОК, чтобы завершить создание нового класса, после чего еще раз нажмите ОК, чтобы закрыть диалоговое окно MFC ClassWizard. Затем при нажатой клавише <Ctrl> еще раз дважды щелкните на элементе управления Edit Box, появится диалоговое окно Add Member Variable, которое вы и ожидали увидеть. Наберите m_FindPrice в поле Member variable name. Убедитесь, что поле Category содержит Value, а поле Variable Type — CString. Нажмите кнопку ОК, чтобы добавить компонентную переменную.
Рис. 21.24. Диалоговое окно "Поиск информации в базе данных" позволит пользователю осуществлять поиск в базе данных
Рис. 21.25. Диалоговое окно Adding a Class
Осталось создать оболочку и саму функцию OnRecordFind():
void CDBView::OnRecordFind()
{
// Создаем экземпляр диалогового окна
CFindPrice dlgFind;
// Нашли соответствующую запись?
BOOL bFound = FALSE;
// Расположение закладки CDBVariant.varBookmark;
// Отображаем диалоговое окно и определяем,
// какую кнопку пользователю нажимать для выхода
if (dlgFind.DoModal () =IDOK)
//Сохраняем текущую позицию, если закладки поддерживаются
if(m_pSet->CanBookmark())
m_pSet->GetBookrnark ( tvarBpokmark);
// Идем к началу
m_pSet->MoveFirst();
// Приводим введенное значение к определенному формату
if(dlgFind.m_FindPrice.Find(.) = = -1)
dlgFind.m_FindPrice += ".0000";
else
dlgFind.mJfindPrice += "00";
// Ищем цену, введенную пользователем
while (!m_pSet->IsEOF() ^ bFound)
{
// Проверяем,. равны ли значения
if(m_pSet->m_UnitPrice.CompareNoCase(dlgFind.m_FindPrice)= =0)
bFound = TRUE;
// Переходим к следующей записи
}else
{
m_pSet->MoveNext();
}
if (bFound)
{
// Отображаем данные о новой записи,
UpdateData(FALSE);
} else
{
// Отображаем сообщение об ошибке, если запись не найдена
MessageBox("Запись не найдена",
"Ошибка",
МВ_ОК | MB_ICONERROR) ;
// Восстанавливаем текущую позицию,
// если база данных поддерживает закладки
if(m_pSet->CanBookmark();
m_pSet->SetBookraark(varBookmark);
else
//В противном случае переходим к первой записи
m_pSet->MoveFirst();
}
}
}
Первое, на что следует обратить внимание в этом коде — мы запоминаем текущую позицию в базе данных, используя для этого закладки. Они работают практически так же, как закладки, которые мы используем для книги, чтобы отметить место, где читаем. Поэтому, используя функцию CanBookmark(), нужно прежде всего убедиться в том, что система управления базами данных поддерживает закладки. Если это так, то можно применять функцию GetBookmark(), чтобы отметить текущую позицию, и функцию SetBookmark (), чтобы восстанавливать ее.
Обратите внимание, что предложенная процедура поиска очень проста. Если мы обнаружили запись, которую затребовал пользователь, мы устанавливаем булевскую переменную на значение "истина", в противном случае идем к следующей записи. Этот процесс продолжается до тех пор, пока мы либо не найдем первую запись, соответствующую пользовательскому критерию, либо не закончим запрос. Если вы устанавливаете аналогичную процедуру поиска, убедитесь, что проверяете оба момента.
Если требуемая запись не будет обнаружена, программа выведет сообщение об ошибке. Она также старается возвратить пользователя к предыдущей записи, если система управления базами данных поддерживает закладки. В противном случае программа возвращает пользователя к первой записи в запросе. Крайне важно установить указатель на какую-нибудь запись, так как в противном случае система управления базами данных, вероятно, перейдет к обработке исключения, сообщая, что запись потеряна.
Примечание
После окончания поиска следует в обязательном порядке переустановить указатель записи, чтобы запрос находился на точно известной записи.
Если мы найдем запись, то должны отобразить информацию в форме. Это делает оставшаяся часть кода, приведенного в этом разделе.
Однако, прежде чем компилировать и выполнять наше приложение, необходимо внести еще одно дополнение — в начале листинга файла <dbview.cpp> расположены директивы #include. Чтобы можно было работать с диалоговым окном FindPrice, необходимо включить в модуль его заголовочный файл. Добавьте новую директиву #include, аналогичную показанной здесь:
// Для поддержки блока диалога FindPrice,
//в начало файла DBView.CPP необходимо
// самостоятельно включить строку
«include "FindPrice.h"
Теперь, когда есть все фрагменты, скомпилируйте и выполните программу. В нашей маленькой таблице процедура поиска выполняется очень быстро, но будет замедляться по мере увеличения размера таблицы. Если вы планируете работать с огромными базами данных, вам потребуется более тщательно подойти к разработке процедуры поиска. Однако и рассмотренная процедура прекрасно работает с таблицей, содержащей около 10 000 записей. Конечно, все это зависит от сложности связей таблицы и числа полей, которые она содержит. Как выглядит созданный блок диалога в действии, показано на рис. 21.26, а результат выполнения поиска — на рис. 21.27.
Рис. 21.26. Блок диалога FindPrice в действии
Рис. 21.27. Результат выполнения поиска в таблице базы данных
Если искомой записи в таблице нет, то появится соответствующее сообщение об ошибке (рис. 21.28).
Рис. 21.28. Запись в таблице не найдена
На первый взгляд все работает хорошо и так, как ожидалось. Однако у реализованного способа поиска есть существенный недостаток (я не имею в виду ограниченность критерия поиска одним полем таблицы). Попробуйте несколько раз запросить поиск продуктов по цене $7. И что вы получите в результате? Правильно, каждый раз наша поисковая система будет находить один единственный продукт, который располагается первым в результирующем наборе. Задачу усовершенствования используемого алгоритма поиска я оставляю вам на самостоятельную проработку.