Delphi и Windows API (Статья)

=== Delphi и Windows API (Статья) ===
Одной из наиболее интересных особенностей Delphi является предоставление, наряду с высокоуровневыми функциями VCL, простого доступа к функциям Windows API. Программист в любой момент волен, в зависимости от стоящей перед ним задачи, выбрать для её решения простые в использовании компоненты, либо реализовать алгоритм, требующий компактности и быстродействия при помощи прямых вызовов API. Более того, как правило, можно не прекращая использовать компоненты и визуальное программирование нанести несколько штрихов при помощи API и добиться максимальной точности решения задачи и быстродействия.
=== Windows - окна и сообщения ===
==== Окна ====
Само название операционной системы Windows говорит, что основным её элементом является окно. Для успешного программирования разработчик должен четко представлять себе, что подразумевается в Windows под этим понятием.
Для начинающих программистов не очевидно, что окнами Windows являются не только главные окна, но и большинство элементов управления в них, таких как поля ввода, списки, кнопки и т.п. Фактически, любой элемент интерфейса, способный получать фокус ввода является окном Windows. Окна могут иметь окно-владельца (Parent window). В этом случае они называются дочерним окном (Child Window) и располагаются на поверхности владельца.
===== Класс окна =====
Поведение окна и его внешний вид определяются классом окна. Класс - это внутренняя структура Windows, описывающая шаблон, на основании которого операционная система создает окна. Перед созданием окна Вы должны зарегистрировать его класс при помощи функции:
function RegisterClassEx(
 const WndClass: TWndClassEx
): ATOM; stdcall;
Функция получает структуру данных TWndClassEx, описанную как:
TWndClassEx = packed record
 cbSize: UINT; // размер структуры, должен быть равен
 // SizeOf(TWndClassEx)
 style: UINT; // стиль класса
 lpfnWndProc: TFNWndProc; // адрес процедуры-обработчика сообщений
 cbClsExtra: Integer; // размер дополнительных данных класса
 cbWndExtra: Integer; // размер дополнительных данных окна
 hInstance: HINST; // идентификатор модуля, в котором
 // находится процедура обработки сообщений
 hIcon: HICON; // идентификатор значка окна
 hCursor: HCURSOR; // курсор, который будет появляться при
 // прохождении указателя мыши над окном
 hbrBackground: HBRUSH; // кисть, используемая при заполнении фона
 // окна
 lpszMenuName: PAnsiChar; // имя ресурса, содержащего меню окна
 lpszClassName: PAnsiChar;// имя класса
 hIconSm: HICON; // маленький значок окна
end;
Рассмотрим ключевые элементы этой структуры подробнее.
* Style - Битовая маска, задающая стиль окна. Стиль - это набор флагов, указывающих системе, какие действия надо предпринимать «по умолчанию» при возникновении тех или иных событий в окне. Стиль класса в основном определяет моменты связанные с прорисовкой окна
* lpfnWndProc - Адрес процедуры-обработчика сообщений. Каждый раз, когда в Windows возникает какое-либо событие, относящееся к окну (например, возникла необходимость в перерисовке или переместился курсор мыши над окном), система формирует сообщение, состоящее из идентификатора и двух целочисленных параметров wParam и lParam, и вызывает эту функцию. Таким образом, функция обработки сообщений и определяет поведение всех окон, созданных с этим стилем.
* hIcon - Идентификатор значка, который будет выводиться в левом верхнем углу окна. Сам значок может быть загружен из ресурса функцией API LoadIcon.
* lpszMenuName - Имя меню. Это меню должно быть оформлено в виде ресурса. Если имя не задано - окно по умолчанию не будет иметь меню
* lpszClassName - Имя класса. В дальнейшем на него можно будет ссылаться при создании новых окон.
Как видно из этой структуры именно на уровне класса окна определяется функция обработки сообщений, которая будет определять поведение всех окон с этим классом.
===== Создание окна =====
После того, как класс зарегистрирован, приложение может создавать окна этого класса функцией:
function CreateWindowEx(
 dwExStyle: DWORD; // расширенный стиль окна
 lpClassName: PChar; // имя класса
 lpWindowName: PChar; // заголовок окна
 dwStyle: DWORD; // стиль окна
 X, Y, nWidth,
 nHeight: Integer; // размеры и позиция на экране
 hWndParent: HWND; // идентификатор окна-владельца
 hMenu: HMENU; // идентификатор меню окна
 hInstance: HINST; // идентификатор модуля, ассоциированного с
 // окном
 lpParam: Pointer // дополнительный параметр, передаваемый в
 // оконную процедуру с сообщением WM_CREATE
): HWND; stdcall;
Функция CreateWindowEx позволяет задать конкретный вид окна и уточнить информацию, полученную от класса окна.
==== Сообщения ====
Сообщения - это базовый механизм информирования программ о событиях, на которые они должны реагировать. Ядром программы является функция обработки сообщений, зарегистрированная в классе окна, которая вызывается ядром Windows при появлении событий, на которые программа должна отреагировать. Получение сообщения окном означает вызов его оконной функции с параметрами, описывающими передаваемое сообщение. Например, сразу после создания окна оно получает сообщение WM_CREATE, при нажатии клавиш на клавиатуре - WM_KEYDOWN, WM_KEYUP, при перемещении мыши WM_MOUSEMOVE и т.п. Без обработки сообщений окно не сможет даже отрисовать себя - рисование выполняется по получению сообщений WM_PAINT, WM_NCPAINT. В программе, написанной с использованием только WinAPI, функция обработки сообщений обычно представляет собой оператор case, альтернативами которого являются различные сообщения, которые эта функция должна обработать.
=== TWinControl - оболочка окна Windows ===
Программирование создания окон и цикла обработки сообщений вручную является непростой и довольно низкоуровневой задачей. VCL реализует классы, позволяющие избежать возникающих при этом сложностей.
Базовым классом, инкапсулирующим окно Windows, является TWinControl. При создании экземпляра наследника этого класса, VCL автоматически регистрирует соответствующий класс окна Windows и создает окно. Благодаря этому, наследники TWinControl могут содержать в себе другие окна и обрабатывать сообщения Windows. Визуальные компоненты, не являющиеся наследниками TWinControl (такие, как TLabel, TSpeedButton) не являются окнами в понимании Windows. Все их события эмулируются компонентом, в который они помещены.
==== Свойство Handle ====
Центральным свойством компонента TWinControl является свойство Handle. Это свойство представляет идентификатор окна Windows, полученного при создании этого компонента. Этот идентификатор можно использовать с любыми функциями Windows API, работающими с окнами. Например, следующий код прокручивает текст в TMemo на одну строку вниз:
procedure TForm1.Button1Click(Sender: TObject);
begin
 PostMessage(Memo1.Handle, WM_VSCROLL, SB_LINEDOWN, 0);
end;
==== Метод CreateParams ====
Перед созданием окна TWinControl вызывает виртуальный метод CreateParams, позволяя программисту задать низкоуровневые параметры создаваемого окна. В процедуру передается структура данных
TCreateParams = record
 Caption: PChar; // Заголовок окна, соответствует параметру
 // lpWindowName
 Style: Longint; // Стиль окна, соответствует параметру dwStyle
 ExStyle: Longint; // Расширенный стиль окна (dwExStyle)
 X, Y: Integer;
 Width, Height: Integer; // Координаты окна
 WndParent: HWND; // Идентификатор окна-владельца (hWndParent)
 Param: Pointer // Дополнительный параметр (lpParam)
 WindowClass: TWndClass; // Структура TWndClass, позволяющая задать
 // параметры класса окна
 WinClassName: array[0..63] of Char; // Имя класса окна
 // (lpClassName)
end;
Наследники TWinControl могут перекрыть CreateParams, создавая окна с требуемыми внешним видом и поведением. Например, требуется создать форму, не имеющую заголовка, однако позволяющую изменять свои размеры. Delphi не предоставляет возможности задать такое поведение визуальными средствами, однако, перекрыв TForm.CreateParams мы легко добиваемся нужного эффекта:
procedure TForm1.CreateParams(var Params: TCreateParams);
begin
 inherited; // Вызываем унаследованный обработчик, позволяя
 // VCL подготовить «типовую» конфигурацию окна
 with Params do
 // И изменяем требуемые параметры
 Style := Style and (not WS_CAPTION) or WS_THICKFRAME or WS_POPUP;
end;
В качестве упражнения рекомендую посмотреть на некоторые фрагменты реализации класса TWinControl, расположенного в модуле Controls.pas. Хорошо видно, что метод CreateWnd сначала вызывает метод CreateParams, заполняющий структуру WndClass с параметрами класса окна и параметры для CreateWindow, а затем регистрирует класс и создает окно. Также очень показателен метод TCustomForm.CreateParams, расположенный в модуле Forms.pas. Хорошо видно, как по свойствам Position, BorderIcons и FormStyle формируется набор флагов стиля окна для функции CreateWindow.
==== Обработка сообщений ====
Каждое окно Windows должно обрабатывать сообщения. VCL берет на себя работу по организации цикла сообщений и их базовой обработке. Для большинства сообщений Windows, которые должно обрабатывать окно, уже предусмотрена обработка «по умолчанию». Сообщения, требующие специфической обработки, приводят к вызовам функций-обработчиков событий, например:
WM_MOUSEMOVE 	OnMouseMove
WM_LBUTTONDOWN, WM_RBUTTONDOWN	OnMouseDown
WM_LBUTTONUP, WM_RBUTTONUP 	OnMouseUp
WM_LBUTTONDBLCLK 	OnDblClick
WM_KEYDOWN 	OnKeyDown
WM_KEYUP 	OnKeyUp
WM_PAINT 	OnPaint
И так далее. Показателен в этом смысле метод WndProc класса TWinControl или его наследников. При этом VCL перед вызовом обработчиков производит «упаковку» параметров сообщений в удобный для обработки и анализа вид. Понимание, какое сообщение Windows вызывает срабатывание того или иного события VCL очень помогает при программировании обработчиков и совершенно необходимо при написании собственных компонентов. Разумеется, предусматривать отдельные обработчики для всех из сотен сообщений, которые могут поступить в окно - значит неоправданно усложнить код VCL. Поэтому для обработки остальных сообщений синтаксис Object Pascal предусматривает создание процедур-обработчиков сообщений. Такие процедуры объявляются как
procedure WMSize(var Message: TWMSize); message WM_SIZE;
В качестве параметра такая функция получает указатель на структуру TMessage, содержащую информацию о сообщении, переданном окну. Для многих часто используемых сообщений в модуле Messages.pas определены структуры, позволяющие более удобно работать с конкретными сообщениями.
Рассмотрим пример окна, обрабатывающего сообщение, не предусмотренное VCL. В качестве сообщения используем WM_HOTKEY. Это сообщение посылается окну, при нажатии зарегистрированной в Windows «горячей клавиши», что позволяет программе реагировать на эту клавишу, даже не имея фокуса ввода.
type
 TForm1 = class(TForm)
 procedure FormCreate(Sender: TObject);
 procedure FormDestroy(Sender: TObject);
 private
 // объявляем процедуру-обработчик
 procedure WMHotKey(var Msg: TWMHotKey); message WM_HOTKEY;
 end;
procedure TForm1.FormCreate(Sender: TObject);
begin
 // Регистрируем "горячую клавишу" Ctrl+Alt+F5
 RegisterHotKey(Handle, 0, MOD_CONTROL or MOD_ALT, VK_F5);
end;
procedure TForm1.WMHotKey(var Msg: TWMHotKey);
begin
 // Эта процедура вызывается при получении окном
 // сообщения WM_HOTKEY
 inherited; // Даем форме обработать сообщение
 // если у неё уже есть его обработчик
 Beep; // Выполняем дополнительные действия
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
 // Отменяем регистрацию "горячей клавиши"
 UnRegisterHotKey(Handle, 0);
end;
Обращаю внимание на вызов унаследованного обработчика в методе WMHotKey. Если Вы не уверены, что хотите запретить обработку предком подобного сообщения - всегда вызывайте унаследованный обработчик. В противном случае, Вы рискуете помешать VCL обрабатывать сообщения. Например, написав свой обработчик WM_PAINT и не вызвав в нем унаследованный Вы полностью заблокируете перерисовку формы средствами VCL.
==== Свойства Controls и Parent ====
Каждый наследник TWinControl может служить контейнером для других компонентов. Для связи между родительским и дочерними компонентами служат свойства Controls и Parent. Каждый визуальный компонент имеет свойство Parent, ссылающееся на оконный компонент, являющийся его владельцем. До тех пор, пока не установлено это свойство, компонент не может обрабатывать сообщения Windows и отображаться на экране. Окно-предок ведет список вставленных в него компонентов, доступ к которому можно получить при помощи его свойства Controls.
=== TForm - просто окно ===
Delphi вводит новую для Windows концепцию - форма. Такого понятия нет в Windows, однако оно довольно часто встречается в высокоуровневых средствах разработки. Форма инкапсулирует окно верхнего уровня (top-level window), которое служит контейнером для остальных визуальных элементов программы. С точки зрения Windows - это такое же окно, как и все остальные, только с соответствующим образом подобранным набором стилей. В частности, на этом уровне реализована поддержка главного меню, управление дочерними окнами (MDI). Как и любое окно TForm имеет свойство Handle, которое может быть использовано в вызовах Windows API. Например, следующий код создает форму в виде пятиконечной звезды:
procedure TForm1.FormCreate(Sender: TObject);
const
 Points: packed array [1..10] of TPoint =
 ((X:50; Y:0), (X:63; Y:33), (X:100; Y:33), (X:72; Y:55),
 (X:90; Y:100), (X:50; Y:72), (X:10; Y:100), (X:28; Y:55),
 (X:0; Y:33), (X:37; Y:33));
begin
 BorderStyle := bsNone;
 Brush.Color := clRed;
 SetWindowRgn(Handle, CreatePolygonRgn(Points, 10, WINDING), TRUE);
end;
Концепция форм помогает разработчику в управлении окнами верхнего уровня, однако многие её понятия (например, главное окно приложения, список окон (Screen.Forms)) не имеет прямых аналогов в Windows API.
=== Графика ===
Программы Windows не имеют возможности прямого вывода информации на экран. Вместо этого, программа, желающая что-либо нарисовать в своем окне должна вызвать одну из функций Windows API, предназначенных для рисования. Такой подход позволяет Windows обеспечить одновременную «жизнь» на одном экране нескольких программ, каждая из которых выводит в своем окне произвольную информацию. Вопросы отсечения информации невидимых участков окон и взаимодействия с конкретным драйвером графического устройства ложатся на операционную систему.
==== Графическая подсистема Windows ====
Базовым понятием графической подсистемы Windows является контекст графического устройства - внутренняя структура, определяющая набор графических объектов, их атрибутов и графических режимов, которые могут повлиять на вывод информации. Контекст может быть связан с различными устройствами, такими, как дисплей, принтер, изображение в памяти, что позволяет использовать одни и те же функции для вывода информации на любое из этих устройств. Разумеется, при этом необходимо учитывать возможности конкретного устройства, такие как разрешение или поддерживаемый набор цветов.
Типичный сценарий вывода информации в Windows выглядит следующим образом:
* Получить контекст устройства
** Сформировать графические объекты с требуемыми атрибутами, такие как шрифт, кисть
** «Включить» объекты в контекст, возможно сохранив при этом старые объекты
** Установить атрибуты контекста (режим заполнения и т.п.), сохранив старые атрибуты
** Используя функции рисования вывести информацию на устройство
** Восстановить атрибуты контекста
** Восстановить графические объекты
** Освободить созданные временные графические объекты
* Освободить контекст устройства
Хорошо видно, что собственно вывод информации занимает далеко не главную часть в общем объеме кода. Большая часть времени уходит на создание, сохранение и восстановление графических объектов. При этом нередки трудно находимые ошибки, приводящие к утечке ресурсов из за не освобожденных объектов.
==== Класс TCanvas ====
VCL предлагает элегантное решение проблемы, описанной в предыдущем разделе - класс TCanvas, инкапсулирующий контекст графического устройства Windows. Вместе с вспомогательными классами TFont, TBrush и т.д., реализующими графические объекты этот класс берет на себя всю работу, связанную с получением и освобождением контекстов устройства его атрибутов и графических объектов. Когда Вы меняете, например, свойства кисти (Canvas.Bush) - TCanvas автоматически создаст новую кисть с нужными атрибутами и включит её в свой контекст, а старую - корректно удалит. Наряду с этим, TCanvas реализует ряд методов, позволяющих рисовать на нем без указания контекста устройства. В итоге - программист может вообще не знать ничего о том, как работает графическая подсистема Windows, используя только методы TCanvas. Все классы VCL, допускающие рисование имеют свойство Canvas, указывающее на автоматически создаваемый экземпляр TCanvas, связанный с ним
==== Использование Windows API с TCanvas ====
Реализовать в TCanvas все функции рисования Windows нереально, т.к. это приведет к резкому усложнению кода VCL. Однако, TCanvas имеет свойство Handle, которое представляет идентификатор графического контекста Windows, ассоциированного с экземпляром класса. Используя его мы можем задействовать все многообразие функций API, предназначенных для рисования. При этом всю работу по установке атрибутов и недопущению утечки ресурсов можно оставить на реализацию TCanvas.
Рассмотрим наиболее употребимые функции Windows API, которые могут быть использованы для рисования.
===== DrawText =====
Функция DrawText объявлена как:
function DrawText(
 hDC: HDC; // Контекст графического устройства
 // (TCanvas.Handle)
 lpString: PChar; // Строка для вывода на экран
 nCount: Integer; // Количество символов в строке, если строка
 // имеет нуль-терминатор – можно передать -1
 var lpRect: TRect; // Прямоугольник, в котором будет отрисована
 // строка
 uFormat: UINT // Флаги форматирования.
): Integer; stdcall; // Функция возвращает высоту текста в пикселях
Эта функция имеет широкие возможности по форматированию текста, задаваемые параметром uFormat. Этот параметр представляет собой набор битовых флагов, задающих способ выравнивания и дополнительные операции над текстом.
Рассмотрим некоторые из этих флагов:
* DT_SINGLELINE - Текст выводится в одну строку, игнорируя символы перевода строки и возврата каретки. С этим флагом могут комбинироваться DT_TOP, DT_BOTTOM и DT_VCENTER, задающие вертикальное выравнивание текста.
* DT_WORDBREAK - Текст разбивается на строки так, чтобы он поместился по ширине в прямоугольнике lpRect. Символы возврата каретки и перевода строки также приводят к переходу на новую строку.
* DT_LEFT, DT_CENTER, DT_RIGHT - Выравнивает выводимый текст по левому краю, по центру или по правому краю.
* DT_END_ELLIPSIS - Если строка не помещается в lpRect её конец заменяется на три точки. Бывает удобно использовать для вывода полей, имеющих значительную длину.
* DT_PATH_ELLIPSIS - Используется с путями, содержащими символ '\'. Если строка не помещается в lpRect - часть её из середины заменяется на три точки.
Также, эта функция может использоваться для расчета высоты или ширины области, занимаемой текстом. Для этого, наряду с остальными флагами форматирования надо добавить флаг DT_CALCRECT. Если выводится одна строка текста, то DrawText изменяет lpRect.Right так, чтобы он вместил всю эту строку, если несколько строк (DT_WORDBREAK) - то изменяется lpRect.Bottom так, чтобы полученный прямоугольник вместил весь текст.
Рассмотрим в качестве примера создание списка, содержащего 2 колонки. Первая из них содержит номер позиции, вторая - сопроводительный текст, высота которого может изменяться. Для создания такого списка разместим на форме TListBox, установив его свойство Style в lbOwnerDrawVariable. После этого создадим следующие обработчики событий:
procedure TForm1.FormCreate(Sender: TObject);
begin
 with ListBox1.Items do
 begin
 // Заполняем список
 Add('Первая позиция');
 Add('Вторая позиция. Её описание имеет значительную длину');
 Add('Третья позиция'#13'В ней'#13'имеются переводы строки');
 Add('Четвертая позиция');
 end;
end;
procedure TForm1.ListBox1MeasureItem(Control: TWinControl;
 Index: Integer; var Height: Integer);
var
 R: TRect;
begin
 // Заполняем R, оставив 20 пикселов для первой колонки
 // и 4 пиксела для учета рамки компонента
 R := Rect(20, 0, ListBox1.Width-4, Height);
 // И вычисляем высоту прямоугольника, требуемую
 // для вывода всего текста
 Height := DrawText(ListBox1.Canvas.Handle,
 PChar(ListBox1.Items[Index]), -1, R,
 DT_CALCRECT or DT_WORDBREAK);
end;
procedure TForm1.ListBox1DrawItem(Control: TWinControl;
 Index: Integer; Rect: TRect; State: TOwnerDrawState);
begin
 with ListBox1 do
 begin
 // Очищаем зону рисования
 Canvas.FillRect(Rect);
 // Выводим номер позиции, центрируя его по вертикали
 DrawText(Canvas.Handle, PChar(IntToStr(Index + 1)), -1, Rect, DT_VCENTER or DT_SINGLELINE);
 // Получаем зону для вывода остального текста
 Inc(Rect.Left, 20);
 // И выводим его
 DrawText(Canvas.Handle, PChar(Items[Index]), -1, Rect, DT_WORDBREAK);
 end;
end;
===== DrawEdge =====
Функция DrawEdge ,бывает очень полезна при написании собственных компонентов. Она автоматизирует задачу рисования трехмерных кнопок, рамок и т.п. Функция объявлена как:
function DrawEdge(
 hdc: HDC; // Графический контекст
 var qrc: TRect; // Координаты рамки
 edge: UINT; // Тип рамки
 grfFlags: UINT // Тип бордюра
): BOOL; stdcall;
Рамка рисуется в виде комбинации из двух прямоугольников - внутреннего (inner) и внешнего (outer). Каждый из них может быть выпуклым (raised), либо вдавленным (sunken). Тип рамки определяется параметром edge, который представляет собой битовую маску из следующих значений:
* BDR_RAISEDINNER, BDR_SUNKENINNER - Задают выпуклый, либо вдавленный внутренний контур
* BDR_RAISEDOUTER, BDR_SUNKENOUTER - Задают выпуклый, либо вдавленный внешний контур
Также имеются предопределенные флаги
* EDGE_BUMP - выпуклая рамка
* EDGE_ETCHED - вдавленная рамка
* EDGE_RAISED - выпуклая кнопка
* EDGE_SUNKEN - вдавленная кнопка
Параметр grfFlags определяет тип бордюра. Это набор следующих флагов
* BF_BOTTOM, BF_TOP, BF_LEFT, BF_RIGHT - Рисуется соответствующая сторона рамки.
* BF_BOTTOMLEFT, BF_BOTTOMRIGHT, BF_TOPLEFT, BF_TOPRIGHT - Рисуется соответствующий угол
* BF_RECT - Рисуется вся рамка
* BF_DIAGONAL, BF_DIAGONAL_ENDBOTTOMLEFT, BF_DIAGONAL_ENDBOTTOMRIGHT, BF_DIAGONAL_ENDTOPLEFT, BF_DIAGONAL_ENDTOPRIGHT - Рисует бордюр по диагонали. Используется для создания кнопок, разбитых по диагонали на две секции
* BF_FLAT, BF_SOFT - Позволяют получить «плоские» кнопки для создания интерфейса в стиле Office 97
* BF_MONO - Рисует одномерную рамку
* BF_MIDDLE - «Заливает» внутреннюю область рамки текущей кистью
* BF_ADJUST - Корректирует qrc так, что после отрисовки он соответствует внутренней области рамки. Удобно для дальнейшего рисования.
Комбинируя флаги в зависимости от текущих свойств компонента можно достаточно просто организовать отрисовку кнопок, реагирующих на различные действия пользователя. Например:
procedure TMyButton.Paint;
var
 Rect: TRect;
 Flags: UINT;
 Edge: UINT;
begin
 Rect := Bounds(Left, Right, Width, Height);
 Flags := BF_RECT or BF_MIDDLE or BF_ADJUST;
 if not Ctl3D then
 Flags := Flags or BF_MONO;
 if Flat then
 Flags := Flags or BF_FLAT;
 if Pressed then
 Edge := EDGE_RAISED
 else
 Edge := EDGE_SUNKEN;
 with Canvas do
 begin
 DrawEdge(Handle, Rect, Edge, Flags);
 DrawText(Handle, PChar(Caption), -1, Rect, DT_SINGLELINE or DT_VCENTER)
 end;
end;
=== Резюме ===
Таким образом, Delphi, позволяя программисту не вдаваться в тонкости реализации Windows API, в то же время не изолирует программиста от него, предоставляя ему возможность в любой момент взять управление программой в свои руки и добиться от программы именно того поведения, которое требуется для решения каждой конкретной задачи. Грамотное использование возможностей API позволяет писать более компактные и быстродействующие программы, а зачастую - и сократить объем кода, необходимый для решения задачи. Для того чтобы иметь такую возможность, программист должен понимать, что лежит за классами и функциями VCL и каким объектам Windows они соответствуют.
Author: Тенцер А. Л.
ID: 02397
Category:Статьи