Переход с Delphi 2010 на Delphi XE7

Чуть больше года назад я писал об успешном переходе с Delphi 7 на Delphi 2010 приложений в нашей компании (Вот и мы перешли на Юникод).

В этой заметке кратенько опишу об особенностях перехода с Delphi 2010 на Delphi XE7.

Требования остались прежними – надо чтобы работало и там, и там. Впоследствии, возможно, с отказом от использования Delphi 2010. (От Delphi 7 мы уже отказались… ну за исключением совсем старых программ, которые не развиваются.)

Алгоритм тоже не претерпел сильных изменений – сначала обновление сторонних компонентов, затем обновление компонент и библиотек, которые уже не поддерживаются первоначальными разработчиками, затем обновление своих исходников.

Черновой переход – компиляция и запуск программ со случайным “прокликиванием” интерфейсов и без прогонки тестов – у меня занял всего несколько часов (менее одного рабочего дня). Тяжелее скучнее всего было делать замены такого плана:

  с DecimalSeparator на
  {$if CompilerVersion >= 24}FormatSettings.{$ifend}DecimalSeparator

потому что надо было:

  • найти номер версии Delphi, где это произошло в RTL (в данном случае это XE3 – произошёл отказ аж от 20 штук глобальных переменных в пользу всего одной);
  • таких подобных мест очень много – кроме групповой замены я себе ещё помечал, что не плохо бы пересмотреть код.

Удивила так называемая нормализация типов TPoint, TRect, TSize (в XE2). В одном месте даже написал такой хелпер (хелпер оказался лишним, всё решилось проще, путём переименования локальной переменной).

Ну и некоторые другие редкие мелочи, типа UNIT_PTR или LRESULT.

Переход пока черновой, ради спортивного интереса. Скорость полной сборки приложений можно сказать, что осталась прежней – я проводил по несколько измерений после разных сценариев использования IDE – разница в среднем на 0.4 сек дольше, чем в Delphi 2010 (редко бывало и наоборот). Размер исполняемого exe-файла, получаемого на выходе – в XE7 в среднем на 2.5 мегабайта больше (в Release-сборке, проверял на семи проектах с размером экзешников от 6 до 19 мегабайт).

При этом, стоит отметить как плюс в сторону IDE – Find Declaration (он же Ctrl+Click) стал стабильно добираться до цели, в Delphi 2010 же он через какое-то время просто отваливается.

Сейчас в XE7 пока достаёт только то, что при открытии проекта IDE сразу загружает все модули,объявленные в dpr-файле. И из-за того, что компоненты в IDE я пока не устанавливал, Delphi выдаёт десятки окон вида:

image

Приходится зажимать Escape и ждать… ну секунд 10, терпимо.

Пока я доволен.

P.S.: не спешно (ну очень не спешно) готовлю обновление для BaseForms и dnSplitter. Ваши комментарии могут сыграть роль волшебного пендаля :с).

Читать на сайте автора.

Про OCIBreak и принудительное прерывание обращения к БД

Я как-то уже не раз писал о том, что мы не используем стандартные компоненты доступа к БД. Почти всё самописное. И работаем мы с Oracle.

Недавно я, наконец-таки, сделал “фишку”, без которой вполне можно жить, но с ней приятнее.

Представьте, что у вас есть запрос к БД, который выполняется длительное время. Ну, например, пользователь указал слишком мягкие критерии для фильтрации данных. Или индекса в БД нет. Или запрос изначально “кривой”. Или всё вместе взятое… Для прерывания выполнения текущего обращения к серверу в OCI есть стандартная функция – OCIBreak.

У нас я реализовывал так: в отдельном потоке запускается запрос к серверу. Если запрос выполняется длительное время, то появляется модальное окошко с кнопкой [Прервать]:

image

По завершению запроса – окошко скрывается. Если пользователь успеет нажать кнопку – вызывается OCIBreak,  и запрос корректно прерывается.

Таймаут до появления такого окошка установлен в две секунды – большинство запросов отрабатывают гораздо быстрее. Но если вдруг пользователь видит это окно, то на интуитивном уровне он может догадаться, что надо как-то ограничивать параметры своего запроса.

Однако иногда бывает так, что OCIBreak не прерывает запрос. Точнее он его прерывает, но приходится долго ждать. Это встречается у нас всё реже, но встречается, обычно в старых запросах, когда клиент говорит серверу – мол сделай то-то, а я подожду. И пока сервер не закончит транзакцию – приложение как бы “висит”. А если пользователь испугался и нажал [Перервать] – начинается откат транзакции. И пользователь снова ждёт, пока сервер не отпустит транзакцию. А приложение – продолжает “висеть”.  И, по хорошему, дождаться бы. Но это раздражает, и есть “продвинутые” пользователи, которые тупо прекращают выполнение программы через диспетчер задач.

Вот для таких, довольно редких случаев, я реализовал дополнительную “фишку” – принудительное прерывание. Работает так: если в течении 5 секунд OCIBreak не отпустил обращение к БД, то кнопка [Перервать] превращается в [Принудительно] и её снова можно нажать.

Что же происходит в этом случае? (Сначала я пробовал убить поток, выполняющий обращение к серверу, но это, конечно же, ничем хорошим не кончилось.)

При нажатии на кнопку [Принудительно] я делаю две вещи:

  • запускаю отдельным потоком вторую сессию к БД и выполняю: alter system kill session »:sid, :serial» immediate;
  • разрываю текущее TCP-соединение на стороне приложения.

Первый пункт нужен, чтобы сервер понял о наших намерениях – мы не собираемся больше ждать. Второй пункт для меня был не тривиальным. Не вдаваясь в подробности поиска решения, привожу код модуля, который у меня получился:

unit MyMIBUtils;

interface

uses
  Windows;

type
  ULONG = Integer;
  PVOID = Pointer;

const
  ANY_SIZE = 1;
  AF_INET = 2;

type
  PMIB_TCPROW = ^MIB_TCPROW;
  _MIB_TCPROW_W2K = packed record
    dwState: DWORD;
    dwLocalAddr: DWORD;
    dwLocalPort: DWORD;
    dwRemoteAddr: DWORD;
    dwRemotePort: DWORD;
  end;
  MIB_TCPROW = _MIB_TCPROW_W2K;
  TMibTcpRow = MIB_TCPROW;
  PMibTcpRow = PMIB_TCPROW;

const
  MIB_TCP_STATE_CLOSED = 1;
  MIB_TCP_STATE_LISTEN = 2;
  MIB_TCP_STATE_SYN_SENT = 3;
  MIB_TCP_STATE_SYN_RCVD = 4;
  MIB_TCP_STATE_ESTAB = 5;
  MIB_TCP_STATE_FIN_WAIT1 = 6;
  MIB_TCP_STATE_FIN_WAIT2 = 7;
  MIB_TCP_STATE_CLOSE_WAIT = 8;
  MIB_TCP_STATE_CLOSING = 9;
  MIB_TCP_STATE_LAST_ACK = 10;
  MIB_TCP_STATE_TIME_WAIT = 11;
  MIB_TCP_STATE_DELETE_TCB = 12;

type
  TCP_TABLE_CLASS = Integer;

const
  TCP_TABLE_BASIC_LISTENER = 0;
  TCP_TABLE_BASIC_CONNECTIONS = 1;
  TCP_TABLE_BASIC_ALL = 2;
  TCP_TABLE_OWNER_PID_LISTENER = 3;
  TCP_TABLE_OWNER_PID_CONNECTIONS = 4;
  TCP_TABLE_OWNER_PID_ALL = 5;
  TCP_TABLE_OWNER_MODULE_LISTENER = 6;
  TCP_TABLE_OWNER_MODULE_CONNECTIONS = 7;
  TCP_TABLE_OWNER_MODULE_ALL = 8;

type
  PMIB_TCPROW_OWNER_PID = ^MIB_TCPROW_OWNER_PID;
  MIB_TCPROW_OWNER_PID = packed record
    dwState: DWORD;
    dwLocalAddr: DWORD;
    dwLocalPort: DWORD;
    dwRemoteAddr: DWORD;
    dwRemotePort: DWORD;
    dwOwningPid: DWORD;
  end;
  TMibTcpRowOwnerPid = MIB_TCPROW_OWNER_PID;
  PMibTcpRowOwnerPid = PMIB_TCPROW_OWNER_PID;

  PMIB_TCPTABLE_OWNER_PID = ^MIB_TCPTABLE_OWNER_PID;
  MIB_TCPTABLE_OWNER_PID = packed record
    dwNumEntries: DWord;
    Table: array [0..ANY_SIZE - 1] of MIB_TCPROW_OWNER_PID ;
  end;
  TMibTcpTableOwnerPid = MIB_TCPTABLE_OWNER_PID;
  PMibTcpTableOwnerPid = PMIB_TCPTABLE_OWNER_PID;


function SetTcpEntry(const pTcpRow: MIB_TCPROW): DWORD; stdcall;
function GetExtendedTcpTable(pTcpTable: PVOID; var dwSize: DWORD; bOrder: BOOL;
  ulAf: ULONG; TableClass: TCP_TABLE_CLASS; Reserved: ULONG): DWORD; stdcall;

function KillProcessAllTCPConnections(AProcessId: DWORD): DWORD;

implementation

const
  iphlpapilib = 'iphlpapi.dll';

function SetTcpEntry; external iphlpapilib name 'SetTcpEntry';
function GetExtendedTcpTable; external iphlpapilib name 'GetExtendedTcpTable';

function KillProcessAllTCPConnections(AProcessId: DWORD): DWORD;
var
  TCPTable: PMibTcpTableOwnerPid;
  Size: DWORD;
  Res: DWORD;
  I: DWORD;
  TCPRow: TMibTcpRow;
begin
  Result := 0;
  TcpTable := nil;
  Size := 0;
  Res := GetExtendedTcpTable(TCPTable, Size, False, AF_INET, TCP_TABLE_OWNER_PID_CONNECTIONS, 0);
  if Res <> ERROR_INSUFFICIENT_BUFFER then
    Exit;
  GetMem(TCPTable, Size);
  try
    Res := GetExtendedTcpTable(TCPTable, Size, False, AF_INET, TCP_TABLE_OWNER_PID_CONNECTIONS, 0);
    if Res <> NO_ERROR then
      Exit;

    for I := 0 to TCPTable^.dwNumEntries - 1 do
      if TCPTable^.Table[I].dwOwningPID = AProcessId then
        with TCPTable^.Table[I] do
        begin
          TCPRow.dwState := MIB_TCP_STATE_DELETE_TCB;
          TCPRow.dwLocalAddr := dwLocalAddr;
          TCPRow.dwLocalPort := dwLocalPort;
          TCPRow.dwRemoteAddr := dwRemoteAddr;
          TCPRow.dwRemotePort := dwRemotePort;
          Res := SetTCPEntry(TCPRow);
          if Res = NO_ERROR then
            Inc(Result);
        end;
  finally
    FreeMem(TCPTable);
  end;
end;

end.

Этот код работает на Windows XP with SP2 и выше.

Соответственно я вызываю:

  KillProcessAllTCPConnections(GetCurrentProcessId);

и все текущие TCP-соединения моего процесса прерываются (а у меня оно всего одно). Получить информацию о TCP-соединении, которое используется именно текущим OCI-обращением к серверу мне не удалось, да я особо и не пытался. Если вдруг понадобится – это можно сделать, просмотрев активные соединения непосредственно до и после коннекта к БД.

 

P.S.: Пару слов про NonBlocking-mode, который есть в OCI. В современном мире многопоточных операционных систем его не рекомендуется использовать вовсе.

Читать на сайте автора.

Delphi Event bus

Решились на работе поддержать Delphi сообщество и начать выкладывать в open source собственные наработки. 

Первой ласточкой суждено было стать пакету DelphiEventBus — реализация паттерна проектирования Event Bus.  
В Jаva мире есть такие пакеты как guava-libraries, но в Delphi ничего похожего найти не удалось. Потому решено было запилить нечто подобное. 

Из статьи Java event bus library comparison можно выцепить характеристики библиотек реализующих шину сообщений. 

Для DelphiEventBus получается следующие:

  • Объявление слушателя — аннотация

  • Синхронность отправки в шину — по умолчанию отправка синхронна. 

  • Асинхронность — в планах на будующее

  • Фильтрация событий — статическая, т.е. у обработчиков в листенера можно задать декларативно фильтры и их значения. При посылки события задаются значения предопределенных фильтров для конкретного события

  • Иерархия событий — да. Событие это объект. Есть базовый класс всех событий. Обработчик может ждать события определенного класса и всех его наследников

  • Строгость ссылочности листенера — строгая. Обязательная дерегистрация. Регистрируются и дерегистрируются  сразу все обработчики в листенере

  • Приоритет обработчиков — отсутствует. Но дополнительно реализован механизм хуков, похожий на виндовый механизм.

  • Реализованы хуки — возможность игнорировать фильтры и выстраивать цепочки обработки. Вызов хуков строго обратен порядку регистрации.

Должно работать на XE3 и выше. Еще бы readme перевести на английский…


Читать на сайте автора.

Коррекция шрифта в Object Inspector’е

Пробую Delphi XE7. Пока нравится. Поставил GExperts – очень уж я привык к сочетаниям Ctrl+Alt+Down и Ctrl+Alt+Up – переход к следующему/предыдущему объявлению идентификатора. CnPackWizards пока не вышел, но и без него вполне можно жить.
И тут вдруг бросилось в глаза, что у Object Inspector’а шрифт не такой, как везде… (везде – Tahoma 8, а тут – Segoe UI 9)
В GExperts вроде и была возможность менять шрифт инспектора, но почему-то пропала. Поэтому “накидал” на скорую руку пакет, исправляющий данную особенность, результат на картинке (было – стало):
image
Исходник пакета доступен на GitHub, или можно скачать zip-архив отсюда – в  папке ObjInspFntChngr. В модуле uObjInspFntChngr.pas через константы:

const
  PreferParentFont = True;
  PreferFontName = 'Tahoma';
  PreferFontSize = 8;

можно настроить шрифт по своему вкусу.

HINT: Возможно пакет поможет тем, у кого масштабирование текста в Windows больше 100%.

Для установки – открыть dproj-файл, правой кнопкой мыши в Project Manager’е – Install. Пакет можно использовать и в предыдущих версиях Delphi, для этого достаточно удалить dproj-файл и открыть пакет через dpk-файл.

Читать на сайте автора.

Helper для TMessage

В предыдущей заметке я упомянул о хелпере для отладки сообщений Windows. Эта заметка будет предельно краткой: ссылки на исходники и скриншот демки.

Исходник: модуль MsgHlpr.pas. Использовать можно так:

procedure TfrmMain.ApplicationEvents1Message(var Msg: TMsg; var Handled: Boolean);
begin
  Memo1.Lines.Add(Msg.ToString);
..

Демо-приложение:

image

Скачать: MsgHlpr.pas + демо — zip-архив, MsgHlpr.pas на GitHub, исходник демо на GitHub, исполняемый exe-файл демки (zip-архив, 447 КБ).

Работает в Delphi 2010 и выше, при желании легко адаптировать и к предыдущим версиям Delphi.

Читать на сайте автора.

Используем макросы в IDE-редакторе Delphi

Занимаясь отладкой процедур, связанных с обработкой сообщений, постоянно приходится делать сопоставление между кодом сообщения и его строковым наименованием. Ну, к примеру, «прилетает» код 6 — это WM_ACTIVATE. Или сложнее: код 274 (0x0112) = WM_SYSCOMMAND.

Мне это порядком надоело — решил сделать Helper для TMessage (попутно и для TMsg). Ну и в планах попробовать сделать Debugger Visualizer.
Однако о самом хелпере я постараюсь написать в следующий раз. В этой заметке хочу описать, как можно использовать кнопочки Record Macro и Playback Macro, которые находятся в левом нижнем углу строки состояния редактора кода.

(Кстати, мне впервые в жизни пришло в голову попробовать их использовать.)

Итак, я хочу получить код вида:
  case Msg of
    WM_NULL: Result := 'WM_NULL';
    WM_CREATE: Result := 'WM_CREATE';
    ...
    WM_APP: Result := 'WM_APP';
  end;

Идём в модуль Messages и копируем оттуда код:
  {$EXTERNALSYM WM_NULL}
  WM_NULL             = $0000;
  {$EXTERNALSYM WM_CREATE}
  WM_CREATE           = $0001;
  ...
  {$EXTERNALSYM WM_APP}
  WM_APP = $8000;

Вставляем в новый модуль и начинаем макрос. По шагам:
  1. Нажимаю Ctrl+F (Панель поиска), указываю пробел, снимаю все флажки. Enter — чтобы запомнилось.
  2. Устанавливаю курсор на первой строке, нажимаю «Record Macro»:.
  3. Ctrl+Y — удаляем строку
  4. Ctrl+Вправо — курсор к началу идентификатора
  5. F3 — поиск до пробела
  6. Влево — курсор к концу идентификатора
  7. Ctrl+Shift+Влево — выделили идентификатор
  8. Ctrl+C — скопировали выделенное в буфер обмена
  9. Повторяем 5. и 6. — курсор к концу идентификатора
  10. Shift+End — выделение до конца строки
  11. Delete — удаляем выделенное
  12. Набираем на клавиатуре
    : Result := ‘
  13. Ctrl+V — вставили скопированное
  14. Набираем на клавиатуре
    ‘;
  15. Home — переход к началу строки
  16. Вниз — переход к следующей строке
  17. Нажимаю «Stop Recording Macro»:.
Макрос готов, теперь просто жамкаем в «Playback Macro»
пока не достигнем нужного результата.

Читать на сайте автора.