Александр Бусаров дата публикации 15-05-2011 12:28
Бывают случаи, когда программисту хочется отказаться от VCL. Чаще всего конечно это бывает из-за экономии, чтобы exe получался не такой большой (что в нынешний век уже практически бессмысленно, только если вы занимаетесь какой-либо особой областью, например демосценой). Но у VCL есть еще одно неприятное ограничение — он однопоточный. Сама Windows прекрасно поддерживает многопоточность, и когда хочется честной параллельной работы окошек, приходится использовать API.
Что обычно делает программист, который всегда работал с VCL, и тут ему нужны окошки на API? Он идет в гугл за примерами, и в мсдн за описанием функций. А в интернете все примеры как назло используют процедурное программирование. И когда программист таки прикручивает подобный код — участок программы с этим кодом превращается в монстра. А самое главное — не совсем понятно как подобный код можно положить на концепцию ооп. Так, например, как это сделано в VCL. Хочется создать окошко через TMyWindow.Create, хочется работать с окошком внутри класса, как в VCL. Хочется, в конце концов, обрабатывать сообщения message методами.
В этой статье я изобрету велосипед постараюсь сделать что-то подобное VCL структуре, а именно — уложить наши окошки в концепцию ООП. Ну и опишу некоторые принципы работы Windows и оконных сообщений. Ну а поскольку я считаю, что проделывать любую работу надо с пользой, то мы постараемся сделать reused friendly код, который можно будет легко использовать и модифицировать в своих будущих проектах, и использовать, использовать, использовать.
Тот, кто хоть однажды делал окошко средствами 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 есть глобальный объект 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-ов у нас по одному на поток — то нужно хранить список, у нас это будет массив, назовем его: 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 аналогичным образом, но так, чтобы при создании окошка оно добавлялось в список текущего 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, чтобы создавались реальные окошки. Делаем.
Функция обработки оконных сообщений одна на все приложение:
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;
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 беда.
Я очень постараюсь в скором времени выложить дополнение к этой статье. Это дополнение будет представлять из себя мой модуль и описание классов, который имеет гораздо более широкие возможности, чем написанный только что "на коленке".
Надеюсь моя статья оказалась полезной, и кто-то по-другому теперь взглянет на обычные вещи ;)
Внимание! Вышло продолжение данного материала "Окна, WinAPI, Delphi. Продолжение".
К материалу прилагаются файлы:
[Потоки (нити) Threads] [Окна, оконные сообщения]
Обсуждение материала [ 12-08-2012 16:38 ] 70 сообщений |