ODBC пример
На этой консультации, мы обобщим всё, что мы знаем на данный момент. Мы напишем программу, которая использует ODBC API. Для простоты я выбирал базу данных Microsoft Access (Microsoft Access 97) для этой программы.
Загрузите пример.
Примечание: Если вы используете windows.inc версии 1.18 или ниже, вы должны прежде устранить небольшой баг. Запустив поиск в файле windows.inc для SQL_NULL_HANDLE, вы найдете строку: SQL_NULL_HANDLE equ 0L
Удалите символ "L" после нуля, вот так: SQL_NULL_HANDLE equ 0
Эта программа построена на базе диалогового окна с простым меню. Когда пользователь выбирает пункт меню "connect", он пытается подключиться к test.mdb, т.е. своей базе данных. После того, как связь будет установлена, программа отобразит конечную полную строку связи возвращенную драйвером После этого, пользователь может выбрать пункт меню "View All Records", чтобы заполнить контрол listview данными из базы. Кроме того, пользователь может выбрать "Query", чтобы найти специфическую запись. Программа представит небольшой диалоговый блок, предлагающий пользователю набрать имя человека, которого он хочет искать. Когда пользователь нажимает кнопку OK или клавишу Enter, программа выполняет запрос выбрать запись(записи), которые сопоставлены имени. Когда пользователь сделал всё, что ему было необходимо, он может выбрать пункт меню "disconnect", чтобы отключиться от базы данных.
Теперь давайте посмотрим на исходный код: .386 .model flat,stdcall include \masm32\include\windows.inc include \masm32\include\kernel32.inc include \masm32\include\odbc32.inc include \masm32\include\comctl32.inc include \masm32\include\user32.inc includelib \masm32\lib\odbc32.lib includelib \masm32\lib\comctl32.lib includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib
IDD_MAINDLG equ 101 IDR_MAINMENU equ 102 IDC_DATALIST equ 1000 IDM_CONNECT equ 40001 IDM_DISCONNECT equ 40002 IDM_QUERY equ 40003 IDC_NAME equ 1000 IDC_OK equ 1001 IDC_CANCEL equ 1002 IDM_CUSTOMQUERY equ 40004 IDD_QUERYDLG equ 102
DlgProc proto hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD QueryProc proto hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD SwitchMenuState proto :DWORD ODBCConnect proto :DWORD ODBCDisconnect proto :DWORD RunQuery proto :DWORD
.data? hInstance dd ? hEnv dd ? hConn dd ? hStmt dd ? Conn db 256 dup(?) StrLen dd ? hMenu dd ? ; handle to the main menu hList dd ? ; handle to the listview control TheName db 26 dup(?) TheSurname db 26 dup(?) TelNo db 21 dup(?) NameLength dd ? SurnameLength dd ? TelNoLength dd ? SearchName db 26 dup(?) ProgPath db 256 dup(?) ConnectString db 1024 dup(?)
.data SQLStatement db "select * from main",0 WhereStatement db " where name=?",0 strConnect db "DRIVER={Microsoft Access Driver (*.mdb)};DBQ=",0 DBName db "test.mdb",0 ConnectCaption db "Complete Connection String",0 Disconnect db "Disconnect successful",0 AppName db "ODBC Test",0 AllocEnvFail db "Environment handle allocation failed",0 AllocConnFail db "Connection handle allocation failed",0 SetAttrFail db "Cannot set desired ODBC version",0 NoData db "You must type the name in the edit box",0 ExecuteFail db "Execution of SQL statement failed",0 ConnFail db "Connection attempt failed",0 AllocStmtFail db "Statement handle allocation failed",0 Heading1 db "Name",0 Heading2 db "Surname",0 Heading3 db "Telephone No.",0
.code start: invoke GetModuleHandle, NULL mov hInstance,eax call GetProgramPath invoke DialogBoxParam, hInstance, IDD_MAINDLG,0,addr DlgProc,0 invoke ExitProcess,eax invoke InitCommonControls DlgProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD .if uMsg==WM_INITDIALOG invoke GetMenu, hDlg mov hMenu,eax invoke GetDlgItem, hDlg, IDC_DATALIST mov hList,eax call InsertColumn .elseif uMsg==WM_CLOSE invoke GetMenuState, hMenu, IDM_CONNECT,MF_BYCOMMAND .if eax==MF_GRAYED invoke ODBCDisconnect, hDlg .endif invoke EndDialog,hDlg, 0 .elseif uMsg==WM_COMMAND .if lParam==0 mov eax,wParam .if ax==IDM_CONNECT invoke ODBCConnect,hDlg .elseif ax==IDM_DISCONNECT invoke ODBCDisconnect,hDlg .elseif ax==IDM_QUERY invoke RunQuery,hDlg .elseif ax==IDM_CUSTOMQUERY invoke DialogBoxParam, hInstance, IDD_QUERYDLG,hDlg, addr QueryProc, 0 .endif .endif .else mov eax,FALSE ret .endif mov eax,TRUE ret DlgProc endp
GetProgramPath proc invoke GetModuleFileName, NULL,addr ProgPath,sizeof ProgPath std mov edi,offset ProgPath add edi,sizeof ProgPath-1 mov al,"\" mov ecx, sizeof ProgPath repne scasb cld mov byte ptr [edi+2],0 ret GetProgramPath endp
SwitchMenuState proc Flag:DWORD .if Flag==TRUE invoke EnableMenuItem, hMenu, IDM_CONNECT, MF_GRAYED invoke EnableMenuItem, hMenu, IDM_DISCONNECT, MF_ENABLED invoke EnableMenuItem, hMenu, IDM_QUERY, MF_ENABLED invoke EnableMenuItem, hMenu, IDM_CUSTOMQUERY, MF_ENABLED .else invoke EnableMenuItem, hMenu, IDM_CONNECT, MF_ENABLED invoke EnableMenuItem, hMenu, IDM_DISCONNECT, MF_GRAYED invoke EnableMenuItem, hMenu, IDM_QUERY, MF_GRAYED invoke EnableMenuItem, hMenu, IDM_CUSTOMQUERY, MF_GRAYED .endif ret SwitchMenuState endp
ODBCConnect proc hDlg:DWORD invoke SQLAllocHandle, SQL_HANDLE_ENV, SQL_NULL_HANDLE, addr hEnv .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO invoke SQLSetEnvAttr, hEnv,SQL_ATTR_ODBC_VERSION, SQL_OV_ODBC3,0 .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO invoke SQLAllocHandle, SQL_HANDLE_DBC, hEnv, addr hConn .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO invoke lstrcpy,addr ConnectString,addr strConnect invoke lstrcat,addr ConnectString, addr ProgPath invoke lstrcat, addr ConnectString,addr DBName invoke SQLDriverConnect, hConn, hDlg, addr ConnectString, sizeof ConnectString, addr Conn, sizeof Conn,addr StrLen, SQL_DRIVER_COMPLETE .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO invoke SwitchMenuState,TRUE invoke MessageBox,hDlg, addr Conn,addr ConnectCaption, MB_OK+MB_ICONINFORMATION .else invoke SQLFreeHandle, SQL_HANDLE_DBC, hConn invoke SQLFreeHandle, SQL_HANDLE_ENV, hEnv invoke MessageBox, hDlg, addr ConnFail, addr AppName, MB_OK+MB_ICONERROR .endif .else invoke SQLFreeHandle, SQL_HANDLE_ENV, hEnv invoke MessageBox, hDlg, addr AllocConnFail, addr AppName, MB_OK+MB_ICONERROR .endif .else invoke SQLFreeHandle, SQL_HANDLE_ENV, hEnv invoke MessageBox, hDlg, addr SetAttrFail, addr AppName, MB_OK+MB_ICONERROR .endif .else invoke MessageBox, hDlg, addr AllocEnvFail, addr AppName, MB_OK+MB_ICONERROR .endif ret ODBCConnect endp
ODBCDisconnect proc hDlg: DWORD invoke SQLDisconnect, hConn invoke SQLFreeHandle, SQL_HANDLE_DBC, hConn invoke SQLFreeHandle, SQL_HANDLE_ENV, hEnv invoke SwitchMenuState, FALSE invoke ShowWindow,hList, SW_HIDE invoke MessageBox,hDlg,addr Disconnect, addr AppName, MB_OK+MB_ICONINFORMATION ret ODBCDisconnect endp
InsertColumn proc LOCAL lvc:LV_COLUMN mov lvc.imask,LVCF_TEXT+LVCF_WIDTH mov lvc.pszText,offset Heading1 mov lvc.lx,150 invoke SendMessage,hList, LVM_INSERTCOLUMN,0,addr lvc mov lvc.pszText,offset Heading2 invoke SendMessage,hList, LVM_INSERTCOLUMN, 1 ,addr lvc mov lvc.pszText,offset Heading3 invoke SendMessage,hList, LVM_INSERTCOLUMN, 3 ,addr lvc ret InsertColumn endp
FillData proc LOCAL lvi:LV_ITEM LOCAL row:DWORD
invoke SQLBindCol, hStmt,1,SQL_C_CHAR, addr TheName, sizeof TheName,addr NameLength invoke SQLBindCol, hStmt,2,SQL_C_CHAR, addr TheSurname, sizeof TheSurname,addr SurnameLength invoke SQLBindCol, hStmt,3,SQL_C_CHAR, addr TelNo, sizeof TelNo,addr TelNoLength mov row,0 .while TRUE mov byte ptr ds:[TheName],0 mov byte ptr ds:[TheSurname],0 mov byte ptr ds:[TelNo],0 invoke SQLFetch, hStmt .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO mov lvi.imask,LVIF_TEXT+LVIF_PARAM push row pop lvi.iItem mov lvi.iSubItem,0 mov lvi.pszText, offset TheName push row pop lvi.lParam invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi mov lvi.imask,LVIF_TEXT inc lvi.iSubItem mov lvi.pszText,offset TheSurname invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi inc lvi.iSubItem mov lvi.pszText,offset TelNo invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi inc row .else .break .endif .endw ret FillData endp
RunQuery proc hDlg:DWORD invoke ShowWindow, hList, SW_SHOW invoke SendMessage, hList, LVM_DELETEALLITEMS,0,0 invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO invoke SQLExecDirect, hStmt, addr SQLStatement, sizeof SQLStatement .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO invoke FillData .else invoke ShowWindow, hList, SW_HIDE invoke MessageBox,hDlg,addr ExecuteFail, addr AppName, MB_OK+MB_ICONERROR .endif invoke SQLCloseCursor, hStmt invoke SQLFreeHandle, SQL_HANDLE_STMT, hStmt .else invoke ShowWindow, hList, SW_HIDE invoke MessageBox,hDlg,addr AllocStmtFail, addr AppName, MB_OK+MB_ICONERROR .endif ret RunQuery endp QueryProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD .if uMsg==WM_CLOSE invoke SQLFreeHandle, SQL_HANDLE_STMT, hStmt invoke EndDialog, hDlg,0 .elseif uMsg==WM_INITDIALOG invoke ShowWindow, hList, SW_SHOW invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO invoke lstrcpy, addr Conn, addr SQLStatement invoke lstrcat, addr Conn, addr WhereStatement invoke SQLBindParameter,hStmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR,25,0, addr SearchName,25, addr StrLen invoke SQLPrepare, hStmt, addr Conn, sizeof Conn .else invoke ShowWindow, hList, SW_HIDE invoke MessageBox,hDlg,addr AllocStmtFail, addr AppName, MB_OK+MB_ICONERROR invoke EndDialog, hDlg,0 .endif .elseif uMsg==WM_COMMAND mov eax, wParam shr eax,16 .if ax==BN_CLICKED mov eax,wParam .if ax==IDC_OK invoke GetDlgItemText, hDlg, IDC_NAME, addr SearchName, 25 .if ax==0 invoke MessageBox, hDlg,addr NoData, addr AppName, MB_OK+MB_ICONERROR invoke GetDlgItem, hDlg, IDC_NAME invoke SetFocus, eax .else invoke lstrlen,addr SearchName mov StrLen,eax invoke SendMessage, hList, LVM_DELETEALLITEMS,0,0 invoke SQLExecute, hStmt invoke FillData invoke SQLCloseCursor, hStmt .endif .else invoke SQLFreeHandle, SQL_HANDLE_STMT, hStmt invoke EndDialog, hDlg,0 .endif .endif .else mov eax,FALSE ret .endif mov eax,TRUE ret QueryProc endp end start
АНАЛИЗ start: invoke GetModuleHandle, NULL mov hInstance,eax call GetProgramPath
Когда программа стартует, она получает описатель экземпляра, затем обнаруживает свой собственный путь. Это делается с расчётом на то, что база данных, test.mdb, находится в той же папке, что и программа. GetProgramPath proc invoke GetModuleFileName, NULL,addr ProgPath,sizeof ProgPath std mov edi,offset ProgPath add edi,sizeof ProgPath-1 mov al,"\" mov ecx,sizeof ProgPath repne scasb cld mov byte ptr [edi+2],0 ret GetProgramPath endp
Функция GetProgramPath вызывает GetModuleFileName, чтобы получить полный путь и имя программы. После этого мы ищем последний символ "\" в этом пути, чтобы отделить имя файла от пути заменяя имя файла нулём. Таким образом мы получили путь к программе в ProgPath.
Программа затем отображает основное диалоговое окно, используя DialogBoxParam. Сначала, как только основное диалоговое окно загружается, мы получаем дескрипторы меню и контрола listview. Затем создаём три столбца в элементе управления listview (поскольку мы знаем заблаговременно, что множество результатов состоит из трех столбцов. Далее мы переходим к заполнению таблицы созданной на первом шаге.)
После этого мы ждем действия пользователя. Если пользователь выбирает "connect" из меню, он вызывает функцию ODBCConnect ODBCConnect proc hDlg:DWORD invoke SQLAllocHandle, SQL_HANDLE_ENV, SQL_NULL_HANDLE, addr hEnv .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
Первое, что мы делаем это выделяем память для идентификатора окружения, используя SQLAllocHandle: invoke SQLSetEnvAttr, hEnv,SQL_ATTR_ODBC_VERSION, SQL_OV_ODBC3,0 .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
После того как мы получили ид. окружения, мы устанавливаем его параметры сообщая о том, что мы будем использовать синтаксис ODBC 3.x вызывая SQLSetEnvAttr: invoke SQLAllocHandle, SQL_HANDLE_DBC, hEnv, addr hConn .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
Если все идет хорошо, то мы можем начать соединение выделив память для идентификатора соединения, используя SQLAllocHandle: invoke lstrcpy,addr ConnectString,addr strConnect invoke lstrcat,addr ConnectString, addr ProgPath invoke lstrcat, addr ConnectString,addr DBName
Конструируем строку соединения, которую мы будем использовать при подключении: invoke SQLDriverConnect, hConn, hDlg, addr ConnectString, sizeof ConnectString, addr Conn, sizeof Conn, addr StrLen, SQL_DRIVER_COMPLETE .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO invoke SwitchMenuState,TRUE invoke MessageBox,hDlg, addr Conn,addr ConnectCaption, MB_OK+MB_ICONINFORMATION
Когда строка соединения - готова, мы вызываем SQLDriverConnect, чтобы попытаться подключаться к test.mdb, используя MS Access драйвер Если файл test.mdb не обнаружен, то драйвер ODBC известит пользователя об этом поскольку мы определили флаг SQL_DRIVER_COMPLETE. Когда ф-я SQLDriverConnect успешно выполнится, переменная Conn заполнится полной строкой связи созданной драйвером Мы отображаем её используя окно сообщений. SwitchMenuState - простая функция, она прорисовывает серым цветом пункты соответствующего меню.
Сейчас связь с базой данных будет установлена до тех пор пока пользователь не разорвёт её.
Когда пользователь выбирает пункт меню "View All Records", процедура диалогового окна вызывает RunQuery: RunQuery proc hDlg:DWORD invoke ShowWindow, hList, SW_SHOW invoke SendMessage, hList, LVM_DELETEALLITEMS,0,0
С Тех пор как элемент управления listview был создан, он оставался невидимым, но теперь самое время, чтобы показать его. Также нам нужно удалить все пункты (если имеются) с контрола listview. invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
Затем, программа выделяет память для идентификатора инструкции. invoke SQLExecDirect, hStmt, addr SQLStatement, sizeof SQLStatement .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
Выполняем инструкцию SQL используя SQLExecDirect. Я решаю здесь использовать SQLExecDirect, поскольку эта инструкция SQL исполняется лишь однажды: invoke FillData
После выполнения инструкции SQL, набор результатов должен быть возвращен. Мы извлекаем данные результатов в элемент управления listview используя функцию FillData: FillData proc LOCAL lvi:LV_ITEM LOCAL row:DWORD
invoke SQLBindCol, hStmt,1,SQL_C_CHAR, addr TheName, sizeof TheName,addr NameLength invoke SQLBindCol, hStmt,2,SQL_C_CHAR, addr TheSurname, sizeof TheSurname,addr SurnameLength invoke SQLBindCol, hStmt,3,SQL_C_CHAR, addr TelNo, sizeof TelNo,addr TelNoLength
Здесь, набор результатов уже возвращен. Нам нужно связать все три колонки набора результатов с буфером. Мы вызываем ф-ю SQLBindCol чтобы сделать это. Имейте в виду, что нам нужен отдельный вызов для каждой колонки, и мы не должны связывать все столбцы: только столбцы, из которых нам нужно получить данные. mov row,0 .while TRUE mov byte ptr ds:[TheName],0 mov byte ptr ds:[TheSurname],0 mov byte ptr ds:[TelNo],0
Мы инициализируем буфер значением NULL в случае, если в столбце(столбцах) нет данных. Лучше использовать длину данных возвращенных в переменных которые мы определили в SQLBindCol. В нашем примере, мы могли бы проверить величины в NameLength, SurnameLength, TelNoLength, чтобы получить фактическую длину возвращенных строк. invoke SQLFetch, hStmt .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO mov lvi.imask,LVIF_TEXT+LVIF_PARAM push row pop lvi.iItem mov lvi.iSubItem,0 mov lvi.pszText, offset TheName push row pop lvi.lParam invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi
Остальное - просто. Вызываем SQLFetch, чтобы извлечь колонку из набора результата а затем сохранить величины в буферах на контроле listview. Когда колонка становится недоступна (мы достигли конца файла), SQLFetch возвращает SQL_NO_DATA и мы выходим из бесконечного цикла. invoke SQLCloseCursor, hStmt invoke SQLFreeHandle, SQL_HANDLE_STMT, hStmt
Когда мы получили набор результатов, мы должны закрыть курсор, используя SQLCloseCursor, а затем освободить идентификатор инструкции используя SQLFreeHandle.
Когда пользователь выбирает пункт меню "Query", программа отображает другое диалоговое окно, приглашающее пользователя ввести имя по которому будет осуществлён поиск. .elseif uMsg==WM_INITDIALOG invoke ShowWindow, hList, SW_SHOW invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO invoke lstrcpy, addr Conn, addr SQLStatement invoke lstrcat, addr Conn, addr WhereStatement invoke SQLBindParameter,hStmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR,25,0, addr SearchName, 25,addr StrLen invoke SQLPrepare, hStmt, addr Conn, sizeof Conn
Первая вещь, которую делает диалоговое окно - показывает элемент listview управления. Затем распределяет память для идентификатора инструкции и наконец, создает инструкцию SQL. Эта инструкция SQL означает вернуть все значения содержащиеся в таблице main, где имя имеет значение ?. select * from main where name=?
Затем, оно вызывает SQLBindParameter, чтобы соединить ид. инструкции с буфером SearchName. Так что когда инструкция SQL выполняется, драйвер ODBC может получить строку, которая ему нужна из SearchName. Потом оно вызывает SQLPrepare, чтобы скомпилировать инструкцию SQL. Здесь отложенное выполнение разумно т.к. мы подготавливаем/компилируем утверждение SQL только раз и затем, мы будем использовать его много раз. Когда инструкция SQL скомпилирована, последующее выполнение - намного быстрее. .if ax==IDC_OK invoke GetDlgItemText, hDlg, IDC_NAME, addr SearchName, 25 .if ax==0 invoke MessageBox, hDlg,addr NoData, addr AppName, MB_OK+MB_ICONERROR invoke GetDlgItem, hDlg, IDC_NAME invoke SetFocus, eax .else
Когда пользователь занес некоторое имя в окно редактирования и нажал ввод, программа извлекает текст из этого окна редактирования и проверяет действительно ли он был введён, если нет - возвращается значение NULL. В этом случае отображается сообщение и устанавливается клавиатурный фокус на окно редактирования, чтобы подсказать пользователю о необходимости ввода имени. invoke lstrlen,addr SearchName mov StrLen,eax invoke SendMessage, hList, LVM_DELETEALLITEMS,0,0 invoke SQLExecute, hStmt invoke FillData invoke SQLCloseCursor, hStmt
Если имя набирается в окне редактирования, мы находим его длину и сохраняем в StrLen для использования драйвером ODBC (вспомните, что фактически мы получили адрес StrLen для ф-ции
SQLBindParameter). Затем мы вызываем ф-ю SQLExecute, передавая ей в качестве входного параметра ид. инструкции, чтобы выполнить подготовленную инструкцию SQL. Когда происходит возврат из ф-ции SQLExecute, мы вызываем FillData, чтобы отобразить результат в элементе управления listview. Поскольку мы не имеем более полезной информации, мы закрываем курсор вызывая SQLCloseCursor.
[C] Iczelion, пер. SheSan