В своей книге “Визуальное программирование” я описывал простую систему лицензирования, в основе которой лежат алгоритмы кодирования аппаратных атрибутов компьютера, на который устанавливается программное обеспечение. Алгоритм простой и надёжный, но, как показала практика, современные тенденции маркетинга требуют более гибкого подхода в лицензировании программного обеспечения: кроме пожизненной лицензии требуются ещё две категории лицензий:

  • Лицензии на определенную версию программы
  • Лицензии на отдельные модули программы

Для этого мной был разработан набор скриптов для My Visual Database, с помощью которого реализуется лицензирование, в том числе версионное и модульное. Благодаря продуманному алгоритму, скрипты могут быть включены в любой ваш проект: при инициализации подсистемы она автоматически создает все необходимые элементы пользовательского интерфейса, включая форму регистрации.

Алгоритм

Алгоритм проверки правильности лицензионного ключа основан на сравнении его в вычисленным значением, которое получается при вычислении хэша MD5 для строкового выражения.

Тип лицензииСостав выраженияПримечание
ПожизненнаяАппаратный ключ + секретное слово
ВерсионнаяАппаратный ключ + секретное слово + номер версииИспользуется номер основной версии, лицензия действует на все субверсии ПО
МодульнаяАппаратный ключ + секретное слово + префикс модулей + список модулейСписок модулей – это список номеров модулей через запятую
Версионно-модульнаяАппаратный ключ + секретное слово + номер версии + префикс модулей + список модулей

Для определения аппаратного ключа используется функция App_GetHWKey(), которая объединяет серийный номер раздела C: и мак-адрес.

function App_GetHWKey: string; 
// возвращает уникальный ключ оборудования
begin
  try
    Result := StrToMD5 ( GetFirstMacAddress + GetHardDiskSerial('c:') );
  except // в редких случаях диска с: может не быть
    RaiseException('App_GetHWKey() - не найден диск C:');
  end;
end;
Code language: Delphi (delphi)

Одного серийного номера раздела С: может быть недостаточно, так как он легко редактируется некоторыми инструментами по обслуживанию дисков. В то же время были случаи, когда пользователь менял сетевые настройки (включал/отключал wifi-адаптер) и при этом менялся мак-адрес, возвращаемый функцией GetFirstMacAddress(), что приводило к сбою системы лицензирования. Такой же сбой могут вызывать анонимайзеры, меняющие мак-адрес. Поэтому при реализации данной функции нужно будет выбрать наименьшее зло. Или придумать более надёжный алгоритм.

Эту функцию я разместил в модуле App, так как планирую использовать её для сетевой идентификации и она может пригодиться в других модулях программы. Там же я добавил функции, которые отвечают за отображение номера версии. Версия из строковой величины была преобразована в два числа: мажорную и минорную часть номера, отделяемые при отображении точкой. За внешний вид версии теперь отвечает функция App_GetVersion().

function App_GetVersion:string; 
// название версии - строка
begin
  Result := IntToStr(APP_VERSION) + '.' + IntToStr(APP_SUBVERSION);
  // если используется модуль лицензирования //
  if LICENSE_TURN_ON and (not Licensed) then
    Result := Result + ' (DEMO)';
end;
Code language: Delphi (delphi)

Тут возникло неудобство в виде обратной зависимости модуля App.pas от License.pas. Зато механизм лицензирования можно легко включать и отключать с помощью константы LICENSE_TURN_ON. Увы, за простоту придется заплатить надёжностью – так как скомпилированные скрипты MVDB слабо защищены, то злоумышленник, прочитав данную статью, может изменить значение данной константы в готовом проекте, поэтому в вашем проекте дайте этой константе другое название.

Основная процедура проверки лицензии License_Check(); теперь выглядит так:

procedure License_Check;
// проверить лицензию и при необходимости вывести различные надписи
var
  tmpKey: string; // кешируем данные
  tmpHWKey: string;
begin
  tmpKey := IniFile_Read(LICENSE_SECT,LICENSE_KEY,''); // кешируем данные
  if tmpKey = '' then
    Licensed := False
  else
  begin
    tmpHWKey := App_GetHWKey;
    // установить глобальную переменную, которая может использоваться для ограничения функциональности
    //
    // полная пожизненная лицензия
    Licensed := tmpKey = StrToMD5 ( tmpHWKey + LICENSE_SECRET_WORD );
    // лицензия на версию
    if not Licensed then
      Licensed := tmpKey = StrToMD5 ( tmpHWKey + LICENSE_SECRET_WORD + IntToStr(APP_VERSION) );
    // пожизненная модульная лицензия
    if not Licensed then
      Licensed :=  License_CheckModules( tmpKey, tmpHWKey + LICENSE_SECRET_WORD );
    // модульная лицензия на версию
    if not Licensed then
      Licensed :=  License_CheckModules( tmpKey, tmpHWKey + LICENSE_SECRET_WORD + IntToStr(APP_VERSION) );
  end;
  // при необходимости менять надписи
  License_UpdateAboutForm; // на форме "О программе"
  App_UpdateMainForm; // на главной форме
end;
Code language: PHP (php)

Для проверки модульной лицензии потребовалась дополнительная функция License_CheckModules(), которая перебирает все возможные варианты сочетаний модулей, чтобы определить, какому из них соответствует данная лицензия.

function License_CheckModules(AKey:string; AData:string):boolean;
// подбор комбинаций (сочетаний) из номеров модулей.
var
  i: integer;
  k: integer; // сколько разрядов
  p: integer; // разряд, который будем увеличивать
  A: array of integer; // хранение сочетания модулей в виде чисел
  s: string; // сборка номеров в виде строки
begin
  License_Modules := '';
  for k := 1 to LICENSE_MODULE_COUNT do
  begin
    SetLength(A, k+1 ); // нулевой элемент не используем
    for i := 1 to k do
      A[i] := i; // первое подмножество
    p := k; // будем увеличивать самый правый разряд
    while p >= 1 do // закончим, если разряды закончились
    begin
      // сформировать сборку в виде строки
      s:='';
      for i:=1 to k do
        s := s + IntToStr(A[i])+',';
      delete(s,length(s),1);
      // проверяем
      Result := AKey = StrToMD5 ( AData + LICENSE_MOD + s );
      if Result then
      begin
        License_Modules := s;
        exit;
      end;
      // выход из цикла, если мы проверили сборку, состоящую из всех элементов
      if k = LICENSE_MODULE_COUNT then
        break;
      // сдвиг указателя, если последний элемент достиг максимума
      if A[k] = LICENSE_MODULE_COUNT then
      begin
        p := p - 1;
      end
      else
        p := k;
      // если указатель не вышел за границы, то генерируем следующее сочетание
      if p >= 1 then
        for i := k downto p do
        begin
          A[i] := A[p] + i - p + 1;
        end;
    end;
  end;
end;
Code language: Delphi (delphi)

Таким образом, система получилась универсальной: одним ключом можно разрешить работу программы с любым сочетанием модулей, с учетом версии или без учета версии. Результат проверки сохраняется в двух переменных: Licensed и License_Modules. Массив License_ModuleNames предназначен для хранения удобочитаемых названий для модулей, список которых формируется с помощью функции License_GetModulesNames().

var
  Licensed: boolean; // проверка, есть ли у программы лицензия
  License_Modules: string; // лицензии модулей: номера через запятую
  License_ModuleNames: array of string; // лицензии модулей: имена

function License_GetModulesNames():string;
// Получить список лицензированных модулей
var
  tmpList: array of string;
  i:integer;
begin
  Result := '';
  tmpList := SplitString(License_Modules,',');
  for i:=0 to Length(tmpList) - 1 do
  begin
    Result := Result + License_ModuleNames[ StrToInt( tmpList[i] ) ] + ', ';
  end;
  if Result <> '' then
    Delete(Result,Length(Result)-1,2);
end;Code language: Delphi (delphi)

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

procedure License_Init;
// инициализация подсиcтсемы лицензирования
begin
  License_CreateRegForm;
  // модули начинаются с единицы.
  SetLength(License_ModuleNames,LICENSE_MODULE_COUNT+1);
  // Названия модулей, отображаются в окне "О программе"
  License_ModuleNames[1] := 'Первый модуль';
  License_ModuleNames[2] := 'Второй модуль';
  License_ModuleNames[3] := 'Третий модуль';
  License_ModuleNames[4] := 'Четвертый модуль';
  License_ModuleNames[5] := 'Пятый модуль';
  // проверка лицензии
  License_Check;
end;
Code language: Delphi (delphi)

Интеграция с приложением

Кроме функции App_GetVersion, имеются ещё несколько процедур, с помощью которых реализовано взаимодействие с подсистемой лицензирования. В частности, на стандартной форме “О программе” с помощью процедуры License_UpdateAboutForm() добавляются сведения о лицензиаре (1), лицензионном ключе (2) и активных модулях (3). А также кнопка для отображения формы регистрации (4).

procedure License_UpdateAboutForm;
// обновить информацию о лицензии на форме "О программе"
var
  tmpLabel: TLabel;
  tmpLabLink: TLabel;
  tmpButton: TButton;
  tmpButOK: TButton;
begin
  // номер версии
  FindC(frmdbCoreAbout,'labVersion',tmpLabel,False);
  if tmpLabel<>nil then
    tmpLabel.Caption := 'Версия '+App_GetVersion;
  // лицензиара и лицензионный ключ
  FindC(frmdbCoreAbout,'LinkLabel1',tmpLabLink); // для привязки
  FindC(frmdbCoreAbout,'Button1',tmpButOK); // для привязки
  //
  FindC(frmdbCoreAbout,'labLicensee',tmpLabel,False);
  if tmpLabel=nil then
  begin
    tmpLabel := TLabel.Create(frmdbCoreAbout);
    with tmpLabel do
    begin
      Name := 'labLicensee';
      Parent := tmpLabLink.Parent;
      Left := tmpLabLink.Left;
      Top := tmpLabLink.Top + 24 + 80 + 40;
      Font.Size := 11;
    end;
  end;
  tmpLabel.Caption := 'Лицензиар: '+IniFile_Read(LICENSE_SECT,LICENSE_LICENSEE,'');
  //
  FindC(frmdbCoreAbout,'labLicenseKey',tmpLabel,False);
  if tmpLabel=nil then
  begin
    tmpLabel := TLabel.Create(frmdbCoreAbout);
    with tmpLabel do
    begin
      Name := 'labLicenseKey';
      Parent := tmpLabLink.Parent;
      Left := tmpLabLink.Left;
      Top := tmpLabLink.Top + 24*2 + 80+40;
      Font.Size := 11;
    end;
  end;
  tmpLabel.Caption := 'Ключ: '+IniFile_Read(LICENSE_SECT,LICENSE_KEY,'');
  //
  FindC(frmdbCoreAbout,'labModuleList',tmpLabel,False);
  if tmpLabel=nil then
  begin
    tmpLabel := TLabel.Create(frmdbCoreAbout);
    with tmpLabel do
    begin
      Name := 'labModuleList';
      Parent := tmpLabLink.Parent;
      Left := tmpLabLink.Left;
      Top := tmpLabLink.Top + 24*3 + 80+40;
      AutoSize := False;
      Width := 400;
      Height := 72;
      WordWrap := True;
      Font.Size := 11;
    end;
  end;
  tmpLabel.Caption := License_GetModulesNames();
  //
  FindC(frmdbCoreAbout,'btnRegister',tmpButton,False);
  if tmpButton = nil then
  begin
    tmpButton := TButton.Create(frmdbCoreAbout);
    with tmpButton do
    begin
      Name := 'btnRegister';
      Parent := tmpButOK.Parent;
      Left := tmpButOK.Left - 134;
      Width := 130;
      Top := tmpButOK.Top;
      Font.Size := 11;
      Default := False;
      Caption := 'Регистраци'+YA;
      OnClick := @License_About_btnRegister_OnClick;
    end;
  end;
//  tmpButton.Visible := not Licensed;
end;
Code language: Delphi (delphi)

Так как форма регистрации создается программно, то её вызов возможен только с помощью функции App_GetFormByName(), которая возвращает форму по имени:

procedure License_About_btnRegister_OnClick( Sender: TObject; var Cancel:boolean );
// обработка нажатия кнопки регистрации на форме "О программе"
begin
  App_GetFormByName('frmAppReg').ShowModal;
end;
Code language: JavaScript (javascript)

Форма регистрации

Программное создание формы – это кропотливый и длительный процесс по сравнению с использованием визуального конструктора форм. Оправдано ли оно? Давайте для начала сравним оба подхода.

ПроцессыВизуальный конструкторПрограммный код
СозданиеБыстрое, сразу виден результатМедленное, для просмотра результата требуется запуск приложения
РедактированиеБыстроеМедленное
Перенос в другие проектыТребуется ручное копирование элементов формы. Быстрое, достаточно скопировать файл со скриптами и подключить его в проект

Как видите, если не требуется перенос функциональности в другие проекты, то проще использовать визуальный конструктор. Но в случае с подсистемой лицензирования я решил сначала с помощью визуального конструктора сделать прототип, а затем реализовать его создание программно. И вот что у меня вышло.

procedure License_CreateRegForm;
// создание формы регистрации
var
  tmpForm:TForm;
  tmpPC:TdbPageControl;
  tmpTab: TdbTabSheet;
  tmpLabel: TLabel; // TdbLabel не использовать - проблемы со стилем
  tmpMemo: TdbMemo;
  tmpCheckBox: TdbCheckBox;
  tmpButton: TdbButton;
  tmpEdit: TdbEdit;
  tmpFileName: string;
begin
  tmpForm := TForm.Create(Application);
  with tmpForm do
  begin
    Name := 'frmAppReg';
    Caption := 'Регистрация ';
    Width := 387 + 16;
    Height := 308;
    BorderStyle := bsDialog;
    Font.Size := 11;
    Font.Name := 'Segoe UI';
    OnShow := @License_RegForm_OnShow;
    Position := poScreenCenter;
  end;
  //
  tmpPC := TdbPageControl.Create(tmpForm);
  with tmpPC do
  begin
    Name := 'pgcMain';
    Parent := tmpForm;
    Align := alClient;
  end;
  //////////////////////////////////////////////////////////////////////////////////////////////
  //
  tmpTab := TdbTabSheet.Create(tmpForm);
  with tmpTab do
  begin
    Name := 'tshLicenseAgr';
    TabVisible := False;
    PageControl := tmpPC;
  end;
  //
  tmpLabel := TLabel.Create(tmpForm);
  with tmpLabel do
  begin
    Name := 'labLicenseAgr';
    Parent := tmpTab;
    Caption := 'Лицензионное соглашение:';
    Left := 8;
    Top := 8;
  end;
  //
  tmpMemo := TdbMemo.Create(tmpForm);
  with tmpMemo do
  begin
    Name := 'memLicenseAgr';
    Parent := tmpTab;
    Left := 8;
    Top := 32;
    Width := 364+4;
    Height := 161;
    ScrollBars := ssVertical;
    Anchors := akTop+akLeft+akRight+akBottom;
    //
    tmpFileName := ExtractFilePath(Application.ExeName) + 'license.txt';
    if FileExists(tmpFileName) then
      Lines.LoadFromFile(tmpFileName)
    else
      RaiseException('Не найден файл license.txt');
  end;
  //
  tmpCheckBox := TdbCheckBox.Create(tmpForm);
  with tmpCheckBox do
  begin
    Name := 'chbAccept';
    Parent := tmpTab;
    Left := 8;
    Top := 200;
    Width := 290;
    Caption := 'Принять лицензионное соглашение';
    OnClick := @License_RegForm_chbAccept_OnClick;
    Anchors := akLeft+akBottom;
  end;
  //
  tmpButton := TdbButton.Create(tmpForm);
  with tmpButton do
  begin
    Name := 'btnNext';
    Parent := tmpTab;
    Left := 238;
    Top := 230;
    Width := 137;
    Height := 32;
    Caption := 'Далее';
    OnClick := @License_RegForm_btnNext_OnClick;
    Anchors := akRight+akBottom;
  end;
  //////////////////////////////////////////////////////////////////////////////////////////////
  //
  tmpTab := TdbTabSheet.Create(tmpForm);
  with tmpTab do
  begin
    Name := 'tshLicenseData';
    TabVisible := False;
    PageControl := tmpPC;
  end;
  //
  tmpLabel := TLabel.Create(tmpForm);
  with tmpLabel do
  begin
    Name := 'labLicensee';
    Parent := tmpTab;
    Caption := 'Лицензиар:';
    Left := 8;
    Top := 8;
  end;
  //
  tmpEdit := TdbEdit.Create(tmpForm);
  with tmpEdit do
  begin
    Name := 'edtLicensee';
    Parent := tmpTab;
    Left := 8;
    Top := 32;
    Width := 364;
  end;
  //
  tmpLabel := TLabel.Create(tmpForm);
  with tmpLabel do
  begin
    Name := 'labRegistrationCode';
    Parent := tmpTab;
    Caption := 'Код регистрации:';
    Left := 8;
    Top := 64;
  end;
  //
  tmpEdit := TdbEdit.Create(tmpForm);
  with tmpEdit do
  begin
    Name := 'edtRegistrationCode';
    Parent := tmpTab;
    Left := 8;
    Top := 88;
    Width := 337;
    Height := 28;
  end;
  //
  tmpButton := TdbButton.Create(tmpForm);
  with tmpButton do
  begin
    Name := 'btnCopyToClipboard';
    Caption := '';
    Parent := tmpTab;
    Left := 346;
    Top := 88;
    Width := 28;
    Height := 28;
    Hint := 'Скопировать в буфер обмена';
    ShowHint := True;
    ImageAlignment := 2; // iaCenter
    OnClick := @License_RegForm_btnCopyToClipboard_OnClick;
  end;
  //
  tmpLabel := TLabel.Create(tmpForm);
  with tmpLabel do
  begin
    Name := 'labLicenseKey';
    Parent := tmpTab;
    Caption := 'Лицензионный ключ:';
    Left := 8;
    Top := 120;
  end;
  //
  tmpEdit := TdbEdit.Create(tmpForm);
  with tmpEdit do
  begin
    Name := 'edtLicenseKey';
    Parent := tmpTab;
    Left := 8;
    Top := 144;
    Width := 364;
  end;
  //
  tmpButton := TdbButton.Create(tmpForm);
  with tmpButton do
  begin
    Name := 'btnBuy';
    Parent := tmpTab;
    Left := 8;
    Top := 230-6;
    Width := 130;
    Height := 32;
    Caption := 'Купить';
    OnClick := @License_RegForm_btnBuy_OnClick;
    Anchors := akLeft+akBottom;
  end;
  //
  tmpButton := TdbButton.Create(tmpForm);
  with tmpButton do
  begin
    Name := 'btnActivate';
    Parent := tmpTab;
    Left := 215;
    Top := 230-6;
    Width := 160;
    Height := 32;
    Caption := 'Активировать';
    OnClick := @License_RegForm_btnActivate_OnClick;
    Anchors := akRight+akBottom;
  end;
  tmpPC.ActivePageIndex := 0;
end;

procedure License_RegForm_btnActivate_OnClick (Sender: TObject; var Cancel: boolean);
// Обработка нажатия кнопки "Активировать"
var
  tmpForm: TForm;
  tmpLicenseKey: TdbEdit;
  tmpLicensee: TdbEdit;
begin
  CForm(Sender,tmpForm);
  FindC(tmpForm,'edtLicenseKey',tmpLicenseKey);
  FindC(tmpForm,'edtLicensee',tmpLicensee);
  // записываем
  IniFile_Write(LICENSE_SECT,LICENSE_LICENSEE,tmpLicensee.Text);
  IniFile_Write(LICENSE_SECT,LICENSE_KEY,tmpLicenseKey.Text);
  // проверяем
  License_Check;
  // смотрим
  if Licensed then
  begin
    ShowMessage('Право владения лицензией подтверждено');
    tmpForm.Close; // закрыть форму
  end
  else
  begin
    // сбрасываем
    IniFile_Write(LICENSE_SECT,LICENSE_LICENSEE,'');
    IniFile_Write(LICENSE_SECT,LICENSE_KEY,'');
    ShowMessage('Требуется актуальный лицензионный ключ');
  end;
end;

procedure License_RegForm_btnBuy_OnClick (Sender: TObject; var Cancel: boolean);
// Обработка нажатия кнопки "Купить"
begin
  OpenURL(LICENSE_BUY_LINK);
end;

// procedure License_RegForm_OnShow (Sender: TObject; Action: string);
procedure License_RegForm_OnShow (Sender: TObject;);
//
var
  tmpForm: TForm;
  tmpPC: TdbPageControl;
  tmpCheck: TdbCheckBox;
  tmpButton: TdbButton;
  tmpLicensee: TdbEdit;
  tmpRegistrationCode: TdbEdit;
  tmpLicenseKey: TdbEdit;
begin
  tmpForm := TForm(Sender);
  FindC(tmpForm,'pgcMain',tmpPC);
  FindC(tmpForm,'chbAccept',tmpCheck);
  FindC(tmpForm,'btnNext',tmpButton);
  FindC(tmpForm,'edtLicensee',tmpLicensee);
  FindC(tmpForm,'edtRegistrationCode',tmpRegistrationCode);
  FindC(tmpForm,'edtLicenseKey',tmpLicenseKey);
  // открыть первую вкладку
  tmpPC.ActivePageIndex := 0;
  tmpCheck.Checked := False;
  tmpButton.Enabled := False;
  // заполнить поля данными
  tmpLicensee.Text := IniFile_Read(LICENSE_SECT,LICENSE_LICENSEE,'');
  tmpRegistrationCode.Text := App_GetHWKey;
  tmpLicenseKey.Text := IniFile_Read(LICENSE_SECT,LICENSE_KEY,'');
end;

procedure License_RegForm_btnNext_OnClick (Sender: TObject; var Cancel: boolean);
// Обработка нажатия кнопки "Далее"
var
  tmpForm: TForm;
  tmpPC: TdbPageControl;
begin
  CForm(Sender,tmpForm);
  FindC(tmpForm,'pgcMain',tmpPC);
  tmpPC.ActivePageIndex := 1;
end;

procedure License_RegForm_chbAccept_OnClick (Sender: TObject);
// разблокировка кнопки чекером
var
  tmpForm:TForm;
  tmpButton:TdbButton;
  tmpChecker: TdbCheckbox;
begin
  tmpChecker := TdbCheckbox(Sender);
  CForm(Sender,tmpForm);
  FindC(tmpForm,'btnNext',tmpButton);
  tmpButton.Enabled := tmpChecker.Checked;
end;

procedure License_RegForm_btnCopyToClipboard_OnClick (Sender: TObject; var Cancel: boolean);
// копирование кода регистрации в буфер обмена
var
  tmpForm:TForm;
  tmpEdit:TdbEdit;
begin
  CForm(Sender,tmpForm);
  FindC(tmpForm,'edtRegistrationCode',tmpEdit);
  tmpEdit.SelectAll;
  tmpEdit.CopyToClipboard;
  tmpEdit.SelLength := 0;
end;
Code language: Delphi (delphi)

Если вы дошли до этого места, значит вас не напугал этот огромный листинг, и вы можете полюбоваться результатом. Форма реализована в виде двухстраничного мастера. На первой странице пользователю предлагается ознакомиться с лицензионным соглашением (1), которое загружается из файла license.txt. Он должен располагаться рядом с исполняемым файлом, а его отсутствие означает нарушение целостности программного комплекса и условий лицензионного соглашения. Разблокировка кнопки “Далее” (3) происходит только после того, как пользователь принял лицензионное соглашение (2).

На второй странице расположено поле ввода лицензиара (1) – владельца лицензии, поле отображения кода регистрации (2) – аппаратного ключа, возвращаемого функцией App_GetHWKey(). Рядом с этим полем есть кнопка для копирования кода регистрации в буфер обмена, что упрощает сам процесс, который подразумевает отправку данного кода вендору, дистрибьютору или реселлеру для получения лицензионного ключа (4) на данную установку программы.

Завершение регистрации происходит при нажатии кнопки “Активировать” (5), а для получения информации о том, где именно можно купить лицензию на программу, служит кнопка “Купить” (6), открывающая в браузере страничку вендора.

Итоги

Модульное лицензирование было добавлено в проект “Производство”. При желании оно может быть использовано и в других проектах, созданных на фреймворке My Visual Database. А небольшие доработки позволят использовать модуль license.pas в проектах Delphi и других проектах, использующих язык Object Pascal.

4 комментария к «Модульное лицензирование»
  1. Добрый день проверьте пожалуйста блок лицензирования, т.к. при запуске приложения лезут ошибки: По отсутствию констант. (кое что нашел-подобрал),
    и в строке “tmpHWKey := Agg_GetHWKey; // установить глобальную переменную, которая может.. ” AGG или APP?

  2. Добрый день.
    При запуске, приложение начинпет “ругаться” на отсутствие некоторых констант. И на данную строчку:.. tmpHWKey := Agg_GetHWKey

  3. Добрый день. При запуске приложения, выскакивают ошибки: по отсутствию констант (какую то часть я дописал), и по строке “… tmpHWKey := Agg_GetHWKey;
    // установить глобальную переменную… “

    1. Вячеслав, я исправил опечатку в статье: вместо Agg_GetHWKey нужно использовать App_GetHWKey. По константам подскажите подробней, чего не хватает, добавлю описание или ссылку.

Добавить комментарий для Master Отменить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *