Rambler's Top100
"Knowledge itself is power"
F.Bacon
Поиск | Карта сайта | Помощь | О проекте | ТТХ  
 Подземелье Магов
  
 

Фильтр по датам

 
 К н и г и
 
Книжная полка
 
 
Библиотека
 
  
  
 


Поиск
 
Поиск по КС
Поиск в статьях
Яndex© + Google©
Поиск книг

 
  
Тематический каталог
Все манускрипты

 
  
Карта VCL
ОШИБКИ
Сообщения системы

 
Форумы
 
Круглый стол
Новые вопросы

 
  
Базарная площадь
Городская площадь

 
   
С Л С

 
Летопись
 
Королевские Хроники
Рыцарский Зал
Глас народа!

 
  
ТТХ
Конкурсы
Королевская клюква

 
Разделы
 
Hello, World!
Лицей

Квинтана

 
  
Сокровищница
Подземелье Магов
Подводные камни
Свитки

 
  
Школа ОБЕРОНА

 
  
Арсенальная башня
Фолианты
Полигон

 
  
Книга Песка
Дальние земли

 
  
АРХИВЫ

 
 

Сейчас на сайте присутствуют:
 
  
 
Во Флориде и в Королевстве сейчас  13:15[Войти] | [Зарегистрироваться]

Окна, WinAPI, Delphi

Александр Бусаров
дата публикации 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. Продолжение".



К материалу прилагаются файлы:


Смотрите также материалы по темам:
[Потоки (нити) Threads] [Окна, оконные сообщения]

 Обсуждение материала [ 12-08-2012 16:38 ] 70 сообщений
  
Время на сайте: GMT минус 5 часов

Если вы заметили орфографическую ошибку на этой странице, просто выделите ошибку мышью и нажмите Ctrl+Enter.
Функция может не работать в некоторых версиях броузеров.

Web hosting for this web site provided by DotNetPark (ASP.NET, SharePoint, MS SQL hosting)  
Software for IIS, Hyper-V, MS SQL. Tools for Windows server administrators. Server migration utilities  

 
© При использовании любых материалов «Королевства Delphi» необходимо указывать источник информации. Перепечатка авторских статей возможна только при согласии всех авторов и администрации сайта.
Все используемые на сайте торговые марки являются собственностью их производителей.

Яндекс цитирования