Будь-яку сучасну програму або програмну технологію можна представити як сукупність програмних "шарів". Кожен з цих шарів проводить свою власну роботу, яка полягає в підвищенні рівня абстракції вироблюваних операцій. Так, самий нижчий шар (шари) вводить поняття, які дозволяють абстрагуватися від використовуваного устаткування; наступний шар (шари) дозволяє програмістові абстрагуватися від складної послідовності викликів функцій, вводячи таке поняття як протокол і так далі Практично в будь-якому сучасному програмному продукті можна виявити і виділити близько десятка послідовних шарів абстракції.
Абстракція від устаткування і низькорівневих протоколів вводиться в ядра операційних систем у вигляді бібліотек api (application program interface). Проте сучасні тенденції приводять до необхідності абстрагування і від самих операційних систем, що дозволяє переносити програми з однієї операційної системи на іншу шляхом простої перекомпіляції (трансльовані програми, в основному, взагалі не вимагають ніяких дій з перенесення).
Абстракцію, яка доступна програмістові у вигляді бібліотек api можна назвати базовою. Це найнижчий рівень абстракції, який доступний для прикладного програмування. На рівні ядра системи доступні і нижчі рівні абстракції, проте для їх використання необхідно розробляти спеціалізовані програми (драйвера, модулі). Базовий рівень абстракції (api) надає максимально широкі можливості для прикладного програмування і є найбільш гнучким. Проте, програмування з використанням api є набагато більш трудомістким і приводить до значно великих об'ємів початкової коди програми, чим програмування з використанням додаткових бібліотек.
Додаткові бібліотеки поставляються з багатьма засобами розробки з метою зменшення трудомісткості і термінів розробки програм, що у результаті приводить до підвищення їх конкурентноспособності. Але застосування додаткових бібліотек абстракцій приводить до різкого збільшення розмірів програм, що відкомпілювалися, із-за того що в програму включається код використовуваних бібліотек, до того ж це включення часто буває неефективним - в програму включаються невживані ділянки коди. Крім того, чим більше рівень абстракції бібліотеки, тим складніше її код, і тим більше труднощів виникає при вирішенні складних завдань. Доводиться враховувати безліч взаємозв'язків і взаємних впливів окремих елементів і процесів бібліотеки один на одного. Крім того, структура і функціональність будь-якої бібліотеки зазвичай розраховується на задоволення всіх потенційно виникаючих завдань, що приводить до її громіздкості і неефективності.
У delphi використовується дуже могутня і складна бібліотека vcl (visual components library), яка окрім безпосередніх абстракцій вводить також і безліч своїх функціональних класів. У цій бібліотеці знаходяться компоненти для візуального відображення інформації, роботи з базами даних, з системними об'єктами, компоненти для роботи з internet-протоколами, класи для написання своїх com-объектов і багато що інше. Модулі бібліотеки підключаються до компіляції в міру необхідності, проте базовий розмір простого діалогового проекту з однією формою перевищує 300кб (із статично ськомпонованной бібліотекою). І такий розмір у багатьох випадках може виявитися дуже великим, особливо якщо програма не вимагає великої функціональності в інтерфейсі.
Для вирішення цієї проблеми можна відмовитися від використання бібліотеки vcl, і програмувати, використовуючи базовий набір функцій win32 api. Проте, якщо при розробці лінійних, недіалогових, нерезидентних програм не виникає ніяких труднощів, то розробка програм, що вимагають активної взаємодії з користувачем або системою, стає трудомісткою. Структурне програмування, що рекомендується в таких випадках, виявляється неефективним і трудомістким.
Дана стаття присвячена проблемі створення і використання компактної об'єктно-орієнтованої бібліотеки, яка б полегшила побудову невеликих і ефективних програм на основі win32 api.
2. Існуючі рішення
Авторові відомо три об'єктно-орієнтовані бібліотеки, які можна розглядати як альтернативу бібліотеці vcl при написанні компактних програм. Це бібліотеки класів xcl, acl і kol. Всі бібліотеки безкоштовні і поставляються в початкових кодах.
Бібліотека acl (api control library) Автор: Олександр Боковіков, Катеринбург, Росія Сторіночка: http://a-press.ur.ru/pc/bokovikov e-mail: abb@adx.ru Класи і модулі: tfont, tfonts, tcontrol, twincontrol, tstdcontrol, tlabel, tedit, tlistbox, tbutton, tcheckbox, tcombobox, tgroupbox, tprogressbar, tkeyboard
Бібліотека kol (key object library) Автор: vladimir kladov (mr.bonanzas) Сторіночка: http://xcl.cjb.net e-mail: bonanzas@xcl.cjb.net Класи і модулі: tobj, tlist, tgraphictool, tcanvas, tcontrol, ttimer, ttrayicon, tstream, tstrlist, tdirlist, tinifile
Як видно із списку приведених для кожної бібліотеки класів, ці бібліотеки предендуют швидше не на допомогу при написанні програм з використанням win32 api, а намагаються створити вищий рівень абстракції чим api, принаймні в графічній частині (особливо це відноситься до xcl). Більш того, ієрархія і перелік об'єктів співпадають з відповідними структурами в бібліотеці vcl, що швидше за все пов'язане з бажанням авторів забезпечити логічну сумісність з vcl при побудові програм на основі цих бібліотек.
Дані бібліотеки не забезпечують мінімального розміру програми, за рахунок того що надають вищий рівень абстракції. Вони є компромісом між програмуванням з використанням vcl і програмування на чистому api.
3. Принципи побудови api-библиотеки
тандартним видом api-программирования є структурне програмування. Приклади такого програмування на win32 api є практично в будь-якій книжці по borland pascal, borland c++, microsoft visual c++ і іншим системам розробки. Безліч прикладів api-программирования на З міститься в постачанні microsoft visual c++.
Структурне програмування з віконними функціями, процедурами обробки команд, не в змозі забезпечити швидку і ефективну розробку програм. У сучасній ситуації більшість програмістів звикли до об'єктно-орієнтованого методу, з можливістю інкапсуляції, спадкоємства і перевизначення методів об'єктів. Таке програмування виявляється найбільш ефективним.
Крім того, для побудови ефективної api-библиотеки перш за все потрібно з'ясувати, які завдання при роботі з win32 api є найбільш трудомісткими. Практика показує, що найбільш незручним і трудомістким елементом є реалізація основного диспетчера логіки програми - віконній функції. Реалізація цієї функції як метод класу, а не простий глобальній функції, дозволила б поліпшити структуру коди і полегшити програмування шляхом інкапсулювання всіх змінних усередині віконного класу.
Програмування може бути ще більш полегшене, є возпользоваться механізмом message-процедур мови object pascal. Виклик цих процедур повністю лежить на компіляторі і кореневому об'єкті tobject і включає методи dispatch, defaulthandler, а також всі методи, оголошені з директивою message. Таке решенієе дозволить повністю відмовитися від громіздкого оператора case у віконній функції.
Враховуючи все вищеперелічене автором була створена компактна бібліотека віконних класів winlite. Ця бібліотека є мінімальною, вона не вводить вищих рівнів абстракції чим існують в win32 api - вона тільки полегшує роботу, перекладом програмування в об'єктно-орієнтоване русло. Розмір бібліотеки дуже невеликий і вся вона поміщається в один модуль. Бібліотека реалізує базовий клас tliteframe і побудовані на основі його віконні класи:
tlitewindow - клас вікна, з можливістю subclass'інга; tlitedialog - клас немодального діалогу; tlitedialogbox - клас модального діалогу. Бібліотека може бути використана спільно з vcl. На перший погляд, це можливість є абсурдною і непотрібною, оскільки про економію розміру в цьому випадку не може бути і мови. Проте, іноді бувають моменти, коли реалізація специфічних віконних елементів на основі об'єктів twincontrol або tcustomcontrol може бути утруднена або неефективна із-за їх складності і неочевидної поведінки. В цьому випадку, можна реалізувати такий елемент на базі класу tlitewindow - він поводитиметься стандартним чином, як і належить поводитися стандартному віконному елементу win32. Завдяки своїй простий архітектурі бібліотека може бути використана в багатопотоковій програмі. Звичайно, ви не зможете викликати методи класів одного потоку з іншого потоку без відповідної синхронізації. Проте, ви можете безперешкодно створювати віконні класи в різних потоках без блокування і синхронізації, а також посилати повідомлення віконних класів в іншому потоці.
Практична рада: при api-программировании програміст винен сам стежити за коректним звільненням численних ресурсів, які займає програма під час виконання. Тому, для полегшення цього завдання використовуйте яку-небудь контролюючу утиліту, наприклад memproof або numega boundschecker. Коректне звільнення зайнятих ресурсів украй необхідне !
Для редагування шаблонів діалогів можна використовувати будь-який редактор ресурсів, наприклад borland resource workshop, правда він декілька незручний, а остаточний результат все одно доводиться коректувати уручну.
Вся документація необхідна для api-программирования міститься в тих, що поставляються компанією microsoft компакт-дисках з документацією під загальною назвою msdn (microsoft developer's network). Існує online-версия документації за адресою http://msdn.microsoft.com. Урізана версія msdn, що містить основні файли допомоги, поставляється з delphi.
Перш ніж ви вирішите працювати над своїм проектом в руслі win32 api, подумайте, а навіщо вам це потрібно? У переважному числі випадків розмір програми не має ніякого значення. Я не хочу сказати, що api-программирование складніше чим vcl-программирование. У багатьох випадках легко вивчити і написати 10 викликів api з купою аргументів і розуміти, що відбувається, чим написати 1 виклик простій, на перший погляд, vcl-инструкции і потім довго досліджувати нетрі vcl у пошуках відповіді. Просто api-программирование - це інша культура, до якої ви, можливо, не звикли. І первинна робота може викликати у вас сильне розчарування. api-программирование вимагає дійшлості, копіткості і уважного вивчення документації.
Ті ж, хто зважився програмувати на api, разом з бібліотекою winlite можуть спільно використовувати невізуальні класи як з складу vcl (модулі sysutils, classes), так і багато сторонніх - природно, що розмір вашої програми при цьому збільшиться.
Невізуальні класи бібліотеки acl - http://a-press.ur.ru/pc/bokovikov Невізуальні класи бібліотеки xcl - http://xcl.cjb.net jedi code library - http://www.delphi-jedi.com Системні компоненти на torry - http://www.torry.ru Заслуговує увагу робота Владимира Кладова по зміні функціональності обов'язкового модуля system.pas. З часів перших версій turbo pascal цей модуль за умовчанням компонується у виконуваний код програми. Код модуля реалізує багато принципів і рішення закладено в синтаксис і логіку мови object pascal, і зміна цього модуля дозволяє модифікувати реалізацію цієї логіки. Таке рішення є специфічним для мови object pascal у відмінність, наприклад, від c/c++, де компілятор і абсолюдно всі модулі ніяк не зв'язані. Зміна модуля system.pas, а саме його розбиття на блоки і скорочення рідко використовуваних ділянок коди дозволило скоротити постійні (не змінні) витрати приблизно на 8 кб. Звичайно, для великих проектів, таке скорочення може бути і непомітним, проте цікавий сам принцип. Модифікований модуль system.pas - http://xcl.cjb.net 4. Бібліотека winlite
//////////////////////////////////////////////////////////////////////////////// // winlite, бібліотека класів і функцій для роботи з win32 api // (c) Микола Мазуркин, 1999-2000 // _____________________________________________________________________________ // Віконні класи ////////////////////////////////////////////////////////////////////////////////
unit winlite;
interface
uses windows, messages;
Структури ініціалізацій Оголошення структур, які використовуються для формування параметрів новостворюваних вікон і діалогів відповідно.
//////////////////////////////////////////////////////////////////////////////// // Параметри для створення вікна //////////////////////////////////////////////////////////////////////////////// type twindowparams = record caption : pchar; style : dword; exstyle : dword; x : integer; у : integer; width : integer; height : integer; wndparent : thandle; wndmenu : thandle; param : pointer; windowclass : twndclass; end;
//////////////////////////////////////////////////////////////////////////////// // Параметри для створення діалогу //////////////////////////////////////////////////////////////////////////////// type tdialogparams = record template : pchar; wndparent : thandle; end;
Декларація базового класу tliteframe Базовий клас для вікон і діалогів. Інкапсулює в собі дескриптор вікна і оголошує загальну віконну процедуру. Реалізує механізм message-процедур.
//////////////////////////////////////////////////////////////////////////////// // tliteframe // _____________________________________________________________________________ // Базовий клас для об'єктів tlitewindow, tlitedialog, tlitedialogbox //////////////////////////////////////////////////////////////////////////////// type tliteframe = class(tobject) private fwndcallback: pointer; fwndhandle : thandle; fwndparent : thandle; function windowcallback(hwnd: hwnd; msg, wparam, lparam:longint): longint; stdcall; protected procedure windowprocedure(var msg: tmessage); virtual; public property wndhandle: thandle read fwndhandle; property wndcallback: pointer read fwndcallback; public constructor create(awndparent: thandle); virtual; destructor destroy; override; end;
Декларація віконного класу tlitewindow Створення унікального класу вікна і створення вікна. Можливість субклассинга стороннього вікна.
//////////////////////////////////////////////////////////////////////////////// // Конструктор //////////////////////////////////////////////////////////////////////////////// constructor tliteframe.create(awndparent: thandle); begin inherited create; // Запам'ятовуємо дескриптор батьківського вікна fwndparent := awndparent; // Створюємо місце під блок зворотного виклику fwndcallback := virtualalloc(nil,12,mem_reserve or mem_commit page_execute_readwrite); // Формуємо блок зворотного виклику asm mov eax, self mov ecx [eax].tliteframe.fwndcallback mov word ptr [ecx+0], $6858 // pop eax mov dword ptr [ecx+2], eax // push _self_ mov word ptr [ecx+6] $e950 // push eax mov eax, offset(tliteframe.windowcallback) sub eax, ecx sub eax, 12 mov dword ptr [ecx+8], eax // jmp tliteframe.windowcallback end; end;
//////////////////////////////////////////////////////////////////////////////// // Деструкція //////////////////////////////////////////////////////////////////////////////// destructor tliteframe.destroy; begin // Знищуємо структуру блоку зворотного виклику virtualfree(fwndcallback, 0, mem_release); // Знищення за умовчанням inherited; end;
//////////////////////////////////////////////////////////////////////////////// // tliteframe // _____________________________________________________________________________ // Функції обробки повідомлень ////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////// // Функція зворотного виклику для отримання віконних повідомлень //////////////////////////////////////////////////////////////////////////////// function tliteframe.windowcallback(hwnd: hwnd; msg, wparam, lparam: integer): longint; var windowmsg : tmessage; begin // Запам'ятовуємо дескриптор вікна, якщо це перший виклик віконної процедури if fwndhandle = 0 then fwndhandle := hwnd; // Формуємо повідомлення windowmsg.msg := msg; windowmsg.wparam := wparam; windowmsg.lparam := lparam; // Обробляємо його windowprocedure(windowmsg); // Повертаємо результат назад системі result := windowmsg.result; end;
//////////////////////////////////////////////////////////////////////////////// // Віртуальна функція для обробки віконних повідомлень //////////////////////////////////////////////////////////////////////////////// procedure tliteframe.windowprocedure(var msg: tmessage); begin // Розподіляємо повідомлення по обробниках dispatch(msg); end;
//////////////////////////////////////////////////////////////////////////////// // Конструктор //////////////////////////////////////////////////////////////////////////////// constructor tlitewindow.create(awndparent: thandle); begin inherited; // Формуємо параметри вікна createwindowparams(fwndparams); // Реєструємо клас вікна registerclass(fwndparams.windowclass); // Створюємо вікно with fwndparams do createwindowex(exstyle, windowclass.lpszclassname, caption style, x, у, width, height, wndparent, wndmenu, hinstance, param ); end;
//////////////////////////////////////////////////////////////////////////////// // Конструктор елементу з субклассингом //////////////////////////////////////////////////////////////////////////////// constructor tlitewindow.createsubclassed(awnd: thandle); begin inherited create(getparent(awnd)); // Зберігаємо віконну функцію fwndsubclass := pointer(getwindowlong(awnd, gwl_wndproc)); // Зберігаємо дескриптор вікна fwndhandle := awnd; // Встановлюємо свою віконну функцію setwindowlong(fwndhandle, gwl_wndproc, dword(wndcallback)); end;
//////////////////////////////////////////////////////////////////////////////// // Деструкція //////////////////////////////////////////////////////////////////////////////// destructor tlitewindow.destroy; begin // Наш об'єкт - об'єкт субклассиннга ? if fwndsubclass = nil then begin // Знищуємо клас вікна unregisterclass(fwndparams.windowclass.lpszclassname, hinstance); // Знищуємо вікно if iswindow(fwndhandle) then destroywindow(fwndhandle); end else // Відновлюємо стару віконну функцію setwindowlong(fwndhandle, gwl_wndproc, dword(fwndsubclass)); // Знищення за умовчанням inherited; end;
//////////////////////////////////////////////////////////////////////////////// // Формування параметрів вікна за умовчанням //////////////////////////////////////////////////////////////////////////////// procedure tlitewindow.createwindowparams(var windowparams: twindowparams); var wndclassname : string; begin // Формуємо ім'я класу str(dword(self), wndclassname); wndclassname := classname+':'+wndclassname; // Заповнюємо інформацію про клас вікна with fwndparams.windowclass do begin style := cs_dblclks; lpfnwndproc := wndcallback; cbclsextra := 0; cbwndextra := 0; lpszclassname := pchar(wndclassname); hinstance := hinstance; hicon := loadicon(0, idi_application); hcursor := loadcursor(0, idc_arrow); hbrbackground := color_btnface + 1; lpszmenuname := ''; end; // Заповнюємо інформацію про вікно with fwndparams do begin wndparent := fwndparent; caption := 'lite window'; style := ws_overlappedwindow or ws_visible; exstyle := 0; x := integer(cw_usedefault); у := integer(cw_usedefault); width := integer(cw_usedefault); height := integer(cw_usedefault); wndmenu := 0; param := nil; end; end;
//////////////////////////////////////////////////////////////////////////////// // tlitewindow // _____________________________________________________________________________ // Функції обробки повідомлень ////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////// // Обробник повідомлень за умовчанням //////////////////////////////////////////////////////////////////////////////// procedure tlitewindow.defaulthandler(var msg); begin // Наш об'єкт - об'єкт субклассиннга ? if fwndsubclass = nil then // Викликаємо системну функцію обробки повідомлень with tmessage(msg) do result := defwindowproc(fwndhandle, msg, wparam, lparam) else // Викликаємо стару віконну функцію обробки повідомлень with tmessage(msg) do result := callwindowproc(fwndsubclass, fwndhandle, msg, wparam, lparam); end;
//////////////////////////////////////////////////////////////////////////////// // Конструктор //////////////////////////////////////////////////////////////////////////////// constructor tlitedialog.create(awndparent: thandle); begin inherited; // Формуємо параметри діалогу createdialogparams(fdlgparams); // Створюємо діалог with fdlgparams do createdialogparam(hinstance, template, wndparent, wndcallback, 0); end;
//////////////////////////////////////////////////////////////////////////////// // Деструкція //////////////////////////////////////////////////////////////////////////////// destructor tlitedialog.destroy; begin // Знищуємо діалог if iswindow(fwndhandle) then destroywindow(fwndhandle); // Знищення за умовчанням inherited; end;
//////////////////////////////////////////////////////////////////////////////// // Формування параметрів діалогу за умовчанням //////////////////////////////////////////////////////////////////////////////// procedure tlitedialog.createdialogparams(var dialogparams: tdialogparams); begin dialogparams.wndparent := fwndparent; dialogparams.template := ''; end;
//////////////////////////////////////////////////////////////////////////////// // Обробка повідомлень за умовчанням //////////////////////////////////////////////////////////////////////////////// procedure tlitedialog.defaulthandler(var msg); begin // Повертані значення за умовчанням with tmessage(msg) do if msg = wm_initdialog then result := 1 else result := 0; end;
Реалізація модального діалогового класу tlitedialogbox
//////////////////////////////////////////////////////////////////////////////// // Формування параметрів діалогу за умовчанням //////////////////////////////////////////////////////////////////////////////// procedure tlitedialogbox.createdialogparams(var dialogparams: tdialogparams); begin dialogparams.wndparent := fwndparent; dialogparams.template := ''; end;
//////////////////////////////////////////////////////////////////////////////// // Активізація модального діалогу //////////////////////////////////////////////////////////////////////////////// function tlitedialogbox.showmodal: integer; begin // Формуємо параметри діалогу createdialogparams(fdlgparams); // Показуємо діалог with fdlgparams do result := dialogboxparam(hinstance, template, wndparent, wndcallback, 0); end;
//////////////////////////////////////////////////////////////////////////////// // Обробка повідомлень за умовчанням //////////////////////////////////////////////////////////////////////////////// procedure tlitedialogbox.defaulthandler(var msg); begin // Повертані значення за умовчанням with tmessage(msg) do if msg = wm_initdialog then result := 1 else result := 0; end;