Версия для печати
Окна, WinAPI, Delphi
http://www.delphikingdom.com/asp/viewitem.asp?catalogID=1433Александр Бусаров
дата публикации 15-05-2011 12:28Когда это все-таки нужно
Бывают случаи, когда программисту хочется отказаться от VCL. Чаще всего конечно это бывает из-за экономии, чтобы exe получался не такой большой (что в нынешний век уже практически бессмысленно, только если вы занимаетесь какой-либо особой областью, например демосценой). Но у VCL есть еще одно неприятное ограничение — он однопоточный. Сама Windows прекрасно поддерживает многопоточность, и когда хочется честной параллельной работы окошек, приходится использовать API.
Как этого добиваются
Что обычно делает программист, который всегда работал с VCL, и тут ему нужны окошки на API? Он идет в гугл за примерами, и в мсдн за описанием функций. А в интернете все примеры как назло используют процедурное программирование. И когда программист таки прикручивает подобный код — участок программы с этим кодом превращается в монстра. А самое главное — не совсем понятно как подобный код можно положить на концепцию ооп. Так, например, как это сделано в VCL. Хочется создать окошко через TMyWindow.Create, хочется работать с окошком внутри класса, как в VCL. Хочется, в конце концов, обрабатывать сообщения message методами.
А чего добьемся мы
В этой статье я изобрету велосипед постараюсь сделать что-то подобное VCL структуре, а именно — уложить наши окошки в концепцию ООП. Ну и опишу некоторые принципы работы Windows и оконных сообщений. Ну а поскольку я считаю, что проделывать любую работу надо с пользой, то мы постараемся сделать reused friendly код, который можно будет легко использовать и модифицировать в своих будущих проектах, и использовать, использовать, использовать.
Как окошки работают в Windows
Тот, кто хоть однажды делал окошко средствами WinAPI, знает, что нужно создать функцию, в которую будут приходить наши сообщения, зарегистрировать класс с этой функцией, после этого создать окошко на основе этого класса, и, наконец, в коде потока сделать бесконечный цикл, который бы только и занимался тем, что выбирал оконные сообщения. И это все очень неудобно. Во-первых, потому что непонятно, как красиво с точки зрения кода (а еще лучше с точки зрения ООП) создать 3, 4 или больше окошек. Цикл то у нас один, и оконная функция тоже одна.
Несмотря на то, что пишут, мол, Windows посылает сообщения окну — это не совсем так. Windows посылает сообщения потоку. Все созданные окна — привязываются к конкретному потоку, в котором вызывалась функция CreateWindow. Важно понимать, что если мы создали окно в текущем потоке, то и все сообщения будут обрабатываться только в контексте данного потока. Именно так Windows и реализует многопоточную работу с окнами. Итак, если у нас в одном потоке создано 10 окошек — то все эти сообщения придут в один поток, и выбирать их надо в одном цикле этого потока. Это далее, при выборке можно задать либо фильтр по хендлу (второй параметр GetMessage), либо в оконной функции становится видно, какому окошку пришло сообщение. Но обрабатывать нужно не сообщения окна, а сообщения потока, т.е. второй параметр GetMessage должен быть у нас 0. И это важно, т.к. мы должны обрабатывать сообщение WM_QUIT. Это сообщение никогда не попадет никакому окну, и единственный возможный вариант выбрать это сообщение — передать 0 вторым параметром функции GetMessage. Зачем же нужно это странное сообщение. А нужно оно чтобы корректно выйти из оконного цикла. Когда мы делаем выборку с GetMessage, то функция всегда возвращает нам true, но когда GetMessage выбирает из очереди WM_QUIT — возвращает false. Если мы захотели вдруг выйти из оконного цикла раз и навсегда — то нам нужно вызывать в этом же потоке PostQuitMessage(ExitCode); и сообщение WM_QUIT станет в очередь текущего потока.
Как работают окошки в VCL
В VCL есть глобальный объект Application. Именно он занимается выборкой оконных сообщений, а входим мы в оконный цикл, когда вызывается Application.Run; Фактически Application должен быть одним на поток, и реализовывать оконный цикл, а внутри оконной функции выбирать сообщения, и передавать их конкретному объекту TWinControl. Но главная проблема VCL в том, что объект один на все приложение. Это классический пример вот этой статьи: http://www.gunsmoker.ru/2011/04/blog-post.html
Помимо этого TApplication слишком оброс другим функционалом, и переписать код VCL для многопоточной поддержки очень сложно. К счастью мы и не будем этого делать, мы же хотим отказаться от VCL.
Размышления об архитектуре
Итак, нам нужно много TApplication-ов. Мы напишем свои TApplication-ы, пусть это будут TApp (такое укороченное название от TApplication, нам ведь не нужна вся функциональность этого монстра). TApp-ов нам надо по одному на поток. WinAPI предоставляет нам функцию GetCurrentThreadId, которая возвращает Id потока. Значит в TApp можно хранить этот Id, а все TApp можно хранить в списке, и выбирать из списка только тот, у которого Id совпадает с Id текущего потока. Выборку сообщений можно сделать также, через TApp.Run, а внутри цикла уже находить окошко, для которого сообщение. Класс окошек, пусть у нас будет TWindow (в отличие от TForm) будет построен примерно так же, как в VCL, и все окошки будут хранится в списке у TApp. Таким образом, TApp будет иметь доступ ко всем окошкам и когда нужно вызывать Dispatch для конкретного окошка. Тем, кто не знает что такое message методы и TObject.Dispatch сюда: http://www.delphikingdom.com/asp/viewitem.asp?catalogid=1390
Это все в общих чертах. Если какие-то детали неясны — надеюсь в реализации станет яснее.
Реализация TApp
Поскольку TApp-ов у нас по одному на поток — то нужно хранить список, у нас это будет массив, назовем его: Applications: array of TApp; Поскольку обращения к этому массиву будут из нескольких потоков — важно синхронизировать доступ к нему. Для этого заведем критическую секцию: AppCS: TRTLCriticalSection;
Все это расположим в implementation секции модуля, чтобы другие модули не имели доступ к нашим данным.
В реализации же класса:
TApp = class (TObject) private FThreadID: Cardinal; public property ThreadID: Cardinal read FThreadID; constructor Create; destructor Destroy; override; end;Нам понадобится поле FThreadID, в котором будем хранить ID потока, в котором создавался TApp, а в конструкторе и деструкторе нам надо будет реализовать добавление и удаление объекта из нашего массива. Я подготовил для этого 2 функции, которые находятся только в implementation модуля:
procedure AddApp(const app: TApp); begin EnterCriticalSection(AppCS); SetLength(Applications, Length(Applications)+1); Applications[Length(Applications)-1]:=app; LeaveCriticalSection(AppCS); end; procedure DelApp(const app: TApp); var i, j: integer; begin EnterCriticalSection(AppCS); for i := 0 to Length(Applications) - 1 do if Applications[i]=app then begin for j := i to Length(Applications) - 2 do Applications[j]:=Applications[j+1]; SetLength(Applications, Length(Applications)-1); Break; end; LeaveCriticalSection(AppCS); end;И через них в конструкторе добавляю self, а в деструкторе удаляю:
constructor TApp.Create; begin FThreadID:=GetCurrentThreadId; AddApp(self); end; destructor TApp.Destroy; begin DelApp(self); inherited; end;Получить указатель на TApp для текущего потока в любом месте программы можно через функцию:
function GetApp: TApp; var i: integer; id: Cardinal; begin id:=GetCurrentThreadId; Result:=nil; EnterCriticalSection(AppCS); for i := 0 to Length(Applications) - 1 do if Applications[i].ThreadID=id then begin Result:=Applications[i]; break; end; LeaveCriticalSection(AppCS); end;Как видно — пока все банально просто.
Реализация TWindow
Теперь добавим TWindow аналогичным образом, но так, чтобы при создании окошка оно добавлялось в список текущего TApp. Поскольку с каждым TApp мы будем работать только в одном потоке, то для списка окошек нам синхронизация не нужна. Изменяем класс TApp вот так:
TApp = class (TObject) private FThreadID: Cardinal; FWindows: array of TWindow; procedure AddWindow(wnd: TWindow); procedure DelWindow(wnd: TWindow); public property ThreadID: Cardinal read FThreadID; constructor Create; destructor Destroy; override; end;Реализация новых методов тривиальна:
procedure TApp.AddWindow(wnd: TWindow); begin SetLength(FWindows, Length(FWindows)+1); FWindows[Length(FWindows)-1]:=wnd; end; procedure TApp.DelWindow(wnd: TWindow); var i, j: integer; begin for i := 0 to Length(FWindows) - 1 do if FWindows[i]=wnd then begin for j := i to Length(FWindows) - 2 do FWindows[j]:=FWindows[j+1]; SetLength(FWindows, Length(FWindows)-1); Break; end; end;А вот и сам TWindow:
TWindow = class (TObject) private FOwnerApp: TApp; public constructor Create; virtual; destructor Destroy; override; end;Чтобы каждый раз не "ездить" в критическую секцию я завел поле FOwnerApp, которое заполняется в конструкторе:
constructor TWindow.Create; begin FOwnerApp:=GetApp; FOwnerApp.AddWindow(self); end;а в деструкторе уже используется:
destructor TWindow.Destroy; begin FOwnerApp.DelWindow(self); inherited; end;В этом коде не хватает одной детали. А именно — мы не возбуждаем исключения если что-то пойдет не так. В TWindow.Create мы полагаем, что в FOwnerApp всегда будет записан корректный указатель, но по коду GetApp видно, что функция может вернуть нам nil. Кроме того у нас в TApp.Create всегда добавляется новый TApp в список, даже если он для данного потока уже существует, что не логично. Поскольку одна из наших целей — минимизировать размер exe — я не стану подключать SysUtils, а просто сделаю пустые классы для исключений:
EAppCreationFailed = class (TObject); EWindowCreationFailed = class (TObject);И добавлю наши классы в конструторы:
constructor TApp.Create; begin if GetApp<>nil then raise EAppCreationFailed.Create; FThreadID:=GetCurrentThreadId; AddApp(self); end; constructor TWindow.Create; begin FOwnerApp:=GetApp; if FOwnerApp=nil then raise EWindowCreationFailed.Create; FOwnerApp.AddWindow(self); end;Внедрение WinAPI
Пока у нас вышла только заготовка для работы с окошками. Теперь нужно привязать все это к WinAPI, чтобы создавались реальные окошки. Делаем.
Функция обработки оконных сообщений одна на все приложение:
function WndProc(handle: HWND; Msg: Cardinal; wPrm: WPARAM; lPrm: LPARAM): integer; stdcall; var app: TApp; wnd: TWindow; wndMsg: TWindowMsg; begin wndMsg.Msg:=Msg; wndMsg.wParam:=wPrm; wndMsg.lParam:=lPrm; wndMsg.Result:=-1; app:=GetApp; if assigned(app) then begin wnd:=app.FindByHandle(handle); if assigned(wnd) then wnd.Dispatch(wndMsg); end; if wndMsg.Result<>0 then Result:=DefWindowProc(handle, msg, wPrm, lPrm) else Result:=0; end;Видно что я добавил TWindowMsg, вот эта структура:
TWindowMsg = packed record Msg : Cardinal; wParam: WPARAM; lParam: LPARAM; Result: Integer; end;Если сообщение обработано — то Result должен быть равен 0, как для винапи.
Так же я добавил метод поиска окошка по хендлу: app.FindByHandle(handle);
Так же для TApp я добавил метод:
procedure TApp.Run; var msg: TMsg; begin while GetMessage(msg, 0, 0, 0) do begin TranslateMessage(msg); DispatchMessage(msg); if Length(FWindows)=0 then PostQuitMessage(0); end; end;Просто оконный цикл, как видим, но с возможностью выхода если у TApp ниодного окошка не осталось.
Пришло время TWindow обрасти мясом кодом.
TWindow = class (TObject) private FOwnerApp: TApp; FHandle: HWND; procedure RegClass; procedure UnregClass; procedure CreateWND; procedure DestroyWND; protected function GetWndClassInfo: TWndClassEx; virtual; function GetExStyle: DWORD; virtual; function GetStyle: DWORD; virtual; procedure WMDestroy(var msg: TWindowMsg); message WM_DESTROY; public property Handle: HWND read FHandle; constructor Create; virtual; destructor Destroy; override; end;Как видно я добавил методов регистрации класса, создания окна, уничтожения классов и уничтожения окна. Так же добавил виртуальных методов, которыми мы в наследниках сможем поварьировать нашим окошком при создании.
Вот так регистрируем класс, если он еще не зарегистрирован:
procedure TWindow.RegClass; var wndClassEx: TWndClassEx; begin if not GetClassInfoEx(HInstance, PChar(ClassName), wndClassEx) then begin wndClassEx:=GetWndClassInfo; if RegisterClassEx(wndClassEx)=0 then raise ERegisterClass.Create; end; end;Вот так создаем окошко:
procedure TWindow.CreateWND; begin FHandle:=CreateWindowEx(GetExStyle, PChar(ClassName), PChar(ClassName), GetStyle, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, HInstance, nil); if FHandle=0 then raise EWindowCreation.Create; end;Простое винапи, которое легко уживается в наших классах.
Все. Теперь если мы напишем вот такой вот dpr файл:
program BlaBla; uses untWndOOP; var app: TApp; wnd: TWindow; begin app:=TApp.Create; wnd:=TWindow.Create; app.Run; app.Free; end.То в принципе наша программа будет работать… Вечно… :)
А все потому что мы не предусмотрели уничтожение наших TWindow, и код вечно находится в app.Run цикле. Итак, нам надо обрабатывать WM_DESTROY для окошек, но этот WM_DESTROY придет внутрь объекта TWindow, а уничтожать объект из его метода нельзя. Поэтому нам придется создать еще один список у TApp объектов, которые ждут удаления, и в конце оконного цикла внутри TApp.Run нам нужно будет удалить наши TWindow.
Реализуем:
Добавляем для TWindow message метод:
procedure TWindow.WMDestroy(var msg: TWindowMsg); begin FOwnerApp.DestroyMe(self); msg.Result:=0; end;Соответственно реализуем DestroyMe у TApp:
procedure TApp.DestroyMe(wnd: TWindow); var i: integer; begin for i := 0 to Length(FDestroyArr) - 1 do if FDestroyArr[i]=wnd then exit; SetLength(FDestroyArr, Length(FDestroyArr)+1); FDestroyArr[Length(FDestroyArr)-1]:=wnd; end;Ну и чуть-чуть меняем метод с циклом Run:
procedure TApp.Run; var msg: TMsg; i: integer; begin while GetMessage(msg, 0, 0, 0) do begin TranslateMessage(msg); DispatchMessage(msg); for i := 0 to Length(FDestroyArr) - 1 do FDestroyArr[i].Free; SetLength(FDestroyArr, 0); if Length(FWindows)=0 then PostQuitMessage(0); end; end;Теперь если мы напишем вот такой код:
program BlaBla; uses untWndOOP; var app: TApp; wnd: TWindow; begin app:=TApp.Create; wnd:=TWindow.Create; app.Run; app.Free; end.Он корректно отработает, и по закрытии окошка наша программа завершится.
А как же многопоточность?
Нам осталось реализовать простенький пример работы окошек в несколько потоков. Для примера я воспользуюсь классом TThread, который находится в Classes.pas. Я понимаю, что этот модуль убивает в корне наш минимализм, но моя задача показать как удобно работают наши окошки. Быть может, у меня найдется время, и я напишу подобную статью по потокам и ООП. Итак, вот код простейшего многопоточного приложения с двумя окошками:
program ThreadedBlaBla; uses Classes, untWndOOP; type TMyThread = class (TThread) protected procedure Execute; override; end; { TMyThread } procedure TMyThread.Execute; begin inherited; TApp.Create; TWindow.Create; GetApp.Run; GetApp.Free; end; var thr: TThread; begin thr:=TMyThread.Create(false); TApp.Create; TWindow.Create; GetApp.Run; GetApp.Free; thr.Free; end.Все. Запускаем, появляется 2 окошка. По закрытии двух окошек работа приложения заканчивается.
И это все?
Выше я говорил о reuse friendly модуле. К сожалению, функционал модуля очень маленький, он как минимум не умеет создавать дочерние окошки. Так же нет никакой работы с глобальными классами Windows. Мы не можем без модификации модуля создать даже кнопку. Этот модуль следует рассматривать лишь как базовый, показывающий, как можно обычное WinAPI красиво обернуть в классы, используя возможности Delphi и этот модуль уже можно легко дорабатывать до удобного полноценного модуля. Связка оконной функции с классами реализована, а значит, мою задачу можно считать решенной. Стандартная VCL Delphi действует очень похожим образом, вот только с многопоточностью у окошек VCL беда.
Я очень постараюсь в скором времени выложить дополнение к этой статье. Это дополнение будет представлять из себя мой модуль и описание классов, который имеет гораздо более широкие возможности, чем написанный только что "на коленке".
Надеюсь моя статья оказалась полезной, и кто-то по-другому теперь взглянет на обычные вещи ;)
Специально для Королевства Delphi
Внимание!
Вышло продолжение данного материала "Окна, WinAPI, Delphi. Продолжение".
К материалу прилагаются файлы:
- Проект BlaBla (17.2 K) обновление от 5/15/2011 7:34:00 AM
- Проект ThreadedBlaBla (57.5 K) обновление от 5/15/2011 7:50:00 AM