среда, 14 марта 2012 г.

Урок 33. Оборотная ведомость - экспорт в Excel



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


Не смотря на то, что потом было еще много "хочу" разной сложности от постановщиков задач, с тех давних пор эта идеология живет вместе со мной.

Только решать задачку складывания одних и тех же цифр в различные результаты (=отчеты), сейчас стало на много проще. Для этого существуют универсальные программы, например: Excel.

Итак, в предыдущем уроке я показал как составить и вывести на экран оборотную ведомость. В таком виде можно посмотреть только небольшой отчет, пару каких-то конкретных цифр. Работать, а тем более изменять информацию на экране, добавлять оформление и т.п. невозможно.

Для того, чтобы экспортировать данные из экранного представления прикладной программы "Расходы", добавьте для начала контекстное меню для формы OborotFrm с одним пунктом "Экспорт":

Подготовьте обработчик события нажатия на этот пункт меню.

Тут я хочу сделать небольшое отступление, озадачив всех вопросом: где располагать процедуру экспорта: в данной форме или в модуле главной формы? В принципе - решать Вам, но! Если эта процедура будет универсальной, способной выполнять возложенные на нее функции не только для отчета "Оборотная ведомость", то удобнее ее расположить в модуле главной формы. Более того, если эта процедура окажется на столько универсальна и хороша, что сможет обслуживать не только эту программу, но и многие другие, тогда ей место - в отдельно лежащей библиотеке, например: в файле Dll, но об этом - несколько позже.

А пока - добавьте в форме OborotFRM Вот такой обработчик:



procedure TOborotFrm.N_ExportClick(Sender: TObject);
begin


  // Экспорт в Эксель
  MainFrm.ExportTo('ОВ', DBGridEh1, MainFrm.XLApp, 'Оборотная ведомость', 'Период: '+FormatDateTime('dd/mm/yy',Date_N.Value)+' - '+FormatDateTime('dd/mm/yy',Date_K.Value));


end;

Что здесь "что", перечислю параметры: название книги Excel, ссылка на GridEh, ссылка на неизвестный нам пока компонент, который еще предстоит расположить в главной форме, заголовок документа, подзаголовок.

На этом форму Oborot можно пока закрыть, поскольку все дальнейшие манипуляции будут происходить в форме MainFrm.


Для начала расположите на ней экземпляр компонента TExcelApplication c вкладки Servers, дав ему имя XLApp.


Затем (внимание!) в секции общедоступных объявлений опишите процедуру:



  public
    { Public declarations }
    Procedure ExportTo(p_WorkSheetName: String;  p_PGridEh: TDBGridEh;  p_XLApp: TExcelApplication;
                          p_RepName: String; p_RepSubTitle: String);


А теперь - собственно цель урока - процедура вывода данных:

Procedure TMainFrm.ExportTo(p_WorkSheetName: String; p_PGridEh: TDBGridEh; p_XLApp: TExcelApplication; p_RepName: String; p_RepSubTitle: String);


var WorkBk: _WorkBook; // определяем WorkBook (книгу) WorkSheet: _WorkSheet; // определяем WorkSheet (лист) I, J: Integer; // Вспомогательные переменные SavePlace: TBookmark; // Закладка begin // Вывод данных из источника объекта DBGridEh в Excel try // Установка закладки (чтобы позже вернуть на нее указатель в сетке) SavePlace := p_PGridEh.DataSource.DataSet.GetBookmark; // Соединяемся с сервером TExcelApplication p_XLApp.Connect; // Добавляем WorkBooks в ExcelApplication p_XLApp.WorkBooks.Add(xlWBatWorkSheet, 0); // Выбираем первую WorkBook WorkBk := p_XLApp.WorkBooks.Item[1]; // Определяем первый WorkSheet WorkSheet := WorkBk.WorkSheets.Get_Item(1) as _WorkSheet; // Заполняем свойства WorkSheet WorkSheet.Name := p_WorkSheetName; // Заголовок отчета J := 1; Worksheet.Cells.Item[J,1].Value:= p_RepName; Worksheet.Cells.Item[J,1].Font.Bold := True; Worksheet.Cells.Item[J,1].Font.size := 16; // Указание периода J := 2; Worksheet.Cells.Item[J,1].Value:= p_RepSubTitle; Worksheet.Cells.Item[J,1].Font.Italic := True; // Заголовки колонок J := 3; p_PGridEh.DataSource.DataSet.First; with p_PGridEh.Columns do begin for i := 1 to Count do if Items[I-1].Visible then begin Worksheet.Cells.Item[J,I].Value:= Items[I-1].Title.Caption; Worksheet.Cells.Item[J,I].Font.Bold := True; Worksheet.Cells.Item[J,I].HorizontalAlignment := xlCenter; end; end; // Собственно вывод данных из источника под сеткой в Excel J := 4; with p_PGridEh.DataSource.DataSet do begin First; while not Eof do begin with p_PGridEh.Columns do begin for I := 1 to Count do if Items[I-1].Visible then Worksheet.Cells.Item[J,I].Value:=FieldByName(Items[I-1].FieldName).AsVariant; end; Inc(J, 1); next; end; end; WorkSheet.Columns.ColumnWidth := 25; // Показываем Excel p_XLApp.Visible[0] := True; // Разрываем связь с сервером p_XLApp.Disconnect; // Возврат на закладку p_PGridEh.DataSource.DataSet.GotoBookmark(SavePlace);


// Удаление закладки p_PGridEh.DataSource.DataSet.FreeBookmark(SavePlace); Except MyMessenger.TitleString:='Проблема'; MyMessenger.MessageString:='Возможно, в Вашем компьютере не установлен MS Excel'; MyMessenger.Buttons:=[mbOk]; MyMessenger.ShowMessage; end; end;

Как всегда я достаточно много вписал комментариев в текст процедуры, но,если все же у Вас остались вопросы, и Вы не получили XLS файла, похожего на этот:


то я, как всегда, жду Ваших вопросов:


мои координаты Вы найдете тут.





вторник, 13 марта 2012 г.

Урок 32. “Уважаемые кроты, а не посчитать ли нам?”

Сегодня я решил поделиться одним из главных секретов, который поведал мне мой гуру однажды: "Хочешь сложный отчет получить  - сделай под него таблицу".

Счастье любого бухгалтера - правильно, без ошибок, посчитанная оборотная ведомость. Без лукавства можно утверждать, что "оборотка" - основной документ. Он служит и для составления Главной книги, и для поиска ошибок...

Это - довольно сложный отчет. Составляется он за период. Поэтому включает в себя для каждого счета развернутые (по дебету и кредиту) остатки на начало периода, обороты за период и развернутые остатки на конец периода. Средством первичного контроля правильности составления оборотной ведомости является присутствие в ней всех счетов и по парное совпадение итогов.

Коль скоро нам предстоит получить столь важный и сложный документ, мы должны иметь в базе данных "Расход", о создании которой я рассказал в уроке 18, одну вспомогательную таблицу "Oborot". Чем она существенно отличается от всех остальных? Она не присутствует в схеме данных, поскольку не имеет связей. А связей она не имеет, поскольку в ней нет ключей, в этой таблице не хранится полезная информация. Она нужна только для того, чтобы временно хранить результаты вычислений.

Структура ее проста:








И я надеюсь, не вызовет вопросов при ее создании.

Теперь вернемся к нашему проекту и добавим в него форму Oborot.pas (имя формы OborotFrm).

Все настройки формы мало чем отличаются от настроек формы main, кроме свойства

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

Не забудьте после добавления формы в проект удалить ее из автоматически создаваемых. Вместо этого в главной форме создайте обработчик пункта меню, предназначенного для создания Оборотной ведомости:



procedure TMainFrm.N3Click(Sender: TObject);      // Пункт меню "Оборотная ведомость"
begin
  Application.CreateForm(TOborotFrm, OborotFrm);  // Создание формы OborotFrm
  OborotFrm.ShowModal;                            // Вывод формы в модальном окне
  OborotFrm.Free;                                 // Освобождение памяти после окончания работы с формой
end;

Не забудьте в разделе implementation главной формы добавить ссылку на модуль формы Oborot.pas:

Uses
  MOs
, Oborot;


Теперь в коде добавленной формы пропишем сразу, чтобы не забыть:


Uses Main;


Это нужно для того, чтобы компонентам доступа к данным дать возможность использовать настройки компонента ADOConnection, расположенного на главной форме.

Традиционные обработчики при старте, активации, закрытии формы и некоторые другие:


procedure TOborotFrm.FormCreate(Sender: TObject);
begin


  // Стартовые значения для дат периода
  Date_N.Value:=Now();
  Date_K.Value:=Now()+1;


  // Подключение компонентов доступа к данным к базе данных
  ADOQuery1.Connection:=MainFrm.ADOConnection1;
  ADOQuery2.Connection:=MainFrm.ADOConnection1;
  ADOCommand1.Connection:=MainFrm.ADOConnection1;
  CDS.Connection:=MainFrm.ADOConnection1;


  // Назначение таблицы компоненту
  CDS.TableName:='Oborot';
end;


procedure TOborotFrm.FormActivate(Sender: TObject);
begin


  // Запрос на первичную очистку таблицы 
  ADOCommand1.CommandText:='DELETE Oborot.* FROM Oborot ';
  ADOCommand1.Execute;
end;


procedure TOborotFrm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  Action:= caFree;
end;


procedure TOborotFrm.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin


  // Обработка нажатия клавиши Escape - закрытие формы
  case Key of            // Start Case


    VK_ESCAPE:
      begin
        close;
      end;


    else


  End;                   // End case
end;


procedure TOborotFrm.MyOkButton1Click(Sender: TObject);
begin
  Close;
end;


А теперь - самое главное - собственно алгоритм вычисления оборотной ведомости, в основу которого положена описанная в уроке 16 формула:


procedure TOborotFrm.CalcOborots(Dn,Dk: TDateTime);
Var
    o: Currency;               // Свернутый остаток
    MyStr: String;             // Для хранения строки SQL запроса


begin


  // Очистка временной таблицы
  ADOCommand1.Execute;
  CDS.Active:=True;
  CDS.Requery();


  // Составление оборотной ведомости




  // Заполнение списка счетов  и стартовых остатков (для выбранной валюты)
  ADOQuery1.Active:=False;      // Деактивация запроса
  ADOQuery1.SQL.Clear;          // Очистка и затем - задание строки SQL
  ADOQuery1.SQL.Add('SELECT * FROM Accounts WHERE Val='+IntToStr(MySelect.MySel_IDVal));
  ADOQuery1.Active:=True;       // Включение запроса
  ADOQuery1.First;              // Переход на первую запись


  While not ADOQuery1.EOF do    // Перебор всех значений, пока не будет достигнут конец файла
  Begin
    CDS.Insert;                 // Добавление строки во вспомогательную таблицу, далее - заполнение полей
    CDS.FieldByName('IDAcc').Value:=ADOQuery1.FieldByName('ID').Value;  // ID счета
    CDS.FieldByName('NameAcc').Text:=ADOQuery1.FieldByName('Name').Text;// Название счета
    o:=ADOQuery1.FieldByName('Ost_D').Value-ADOQuery1.FieldByName('Ost_K').Value; // Вычисление свернутого остатка на момент старта программы
    If o>=0
    Then
    Begin
      CDS.FieldByName('OstD_N').Value:=o;
      CDS.FieldByName('OstK_N').Value:=0;      // Это - ноль
    End
    else
    Begin
      CDS.FieldByName('OstD_N').Value:=0;      // Это - ноль
      CDS.FieldByName('OstK_N').Value:=-o;
    End;


      // Вычисление остатков на начало по данным до даты Dn
    o:=CDS.FieldByName('OstD_N').Value-CDS.FieldByName('OstK_N').Value;


    ADOQuery2.Active:=False;
    ADOQuery2.SQL.Clear;
    MyStr:='SELECT Sum(Main.Summa) AS [MySum], IIf(IsNull([MySum]),0,[MySum]) AS MySumNotNull FROM Main WHERE (((Main.MyDate)<#';
    MyStr:=MyStr+FormatDateTime('mm/dd/yyyy',Dn);
    MyStr:=MyStr+'#) AND ((Main.D)=';
    MyStr:=MyStr+IntToStr(CDS.FieldByName('IDAcc').value);
    MyStr:=MyStr+'))';
    ADOQuery2.SQL.Add(MyStr);
    ADOQuery2.Active:=True;


    if ADOQuery2.RecordCount>0
    then
      o:=o+ADOQuery2.FieldByName('MySumNotNull').Value
    else
      o:=0;                                  // Это - ноль


    ADOQuery2.Active:=False;
    ADOQuery2.SQL.Clear;
    MyStr:='SELECT Sum(Main.Summa) AS [MySum], IIf(IsNull([MySum]),0,[MySum]) AS MySumNotNull FROM Main WHERE (((Main.MyDate)<#';
    MyStr:=MyStr+FormatDateTime('mm/dd/yyyy',Dn);
    MyStr:=MyStr+'#) AND ((Main.K)=';
    MyStr:=MyStr+IntToStr(CDS.FieldByName('IDAcc').value);
    MyStr:=MyStr+'))';
    ADOQuery2.SQL.Add(MyStr);
    ADOQuery2.Active:=True;


    if ADOQuery2.RecordCount>0
    then
      o:=o-ADOQuery2.FieldByName('MySumNotNull').Value
    else
      o:=0;                                // Это - ноль


    // Запись остатков на начало
    If o>=0
    Then
    Begin
      CDS.FieldByName('OstD_N').Value:=o;
      CDS.FieldByName('OstK_N').Value:=0;
    End
    else
    Begin
      CDS.FieldByName('OstD_N').Value:=0;
      CDS.FieldByName('OstK_N').Value:=-o;
    End;


    // Вычисление и запись оборотов
    ADOQuery2.Active:=False;
    ADOQuery2.SQL.Clear;
    MyStr:='SELECT Sum(Main.Summa) AS [MySum], IIf(IsNull([MySum]),0,[MySum]) AS MySumNotNull FROM Main ';
    MyStr:=MyStr+'WHERE (((Main.MyDate)>=#'+FormatDateTime('mm/dd/yyyy',Dn)+'#) ';
    MyStr:=MyStr+'AND ((Main.MyDate)<#'+FormatDateTime('mm/dd/yyyy',Dk)+'#) ';
    MyStr:=MyStr+'AND ((Main.D)=';
    MyStr:=MyStr+IntToStr(CDS.FieldByName('IDAcc').value);
    MyStr:=MyStr+'))';
    ADOQuery2.SQL.Add(MyStr);
    ADOQuery2.Active:=True;


    if ADOQuery2.RecordCount>0
    then
      o:=o+ADOQuery2.FieldByName('MySumNotNull').Value
    else
      o:=0;


    CDS.FieldByName('ObD').Value:=ADOQuery2.FieldByName('MySumNotNull').Value;


    ADOQuery2.Active:=False;
    ADOQuery2.SQL.Clear;
    MyStr:='SELECT Sum(Main.Summa) AS [MySum], IIf(IsNull([MySum]),0,[MySum]) AS MySumNotNull FROM Main ';
    MyStr:=MyStr+'WHERE (((Main.MyDate)>=#'+FormatDateTime('mm/dd/yyyy',Dn)+'#) ';
    MyStr:=MyStr+'AND ((Main.MyDate)<#'+FormatDateTime('mm/dd/yyyy',Dk)+'#) ';
    MyStr:=MyStr+'AND ((Main.K)=';
    MyStr:=MyStr+IntToStr(CDS.FieldByName('IDAcc').value);
    MyStr:=MyStr+'))';
    ADOQuery2.SQL.Add(MyStr);
    ADOQuery2.Active:=True;


    if ADOQuery2.RecordCount>0
    then
      o:=o-ADOQuery2.FieldByName('MySumNotNull').Value
    else
      o:=0;


    CDS.FieldByName('ObK').Value:=ADOQuery2.FieldByName('MySumNotNull').Value;


    // Вычисление и запись остатков на конец
    If o>=0
    Then
    Begin
      CDS.FieldByName('OstD_K').Value:=o;
      CDS.FieldByName('OstK_K').Value:=0;
    End
    else
    Begin
      CDS.FieldByName('OstD_K').Value:=0;
      CDS.FieldByName('OstK_K').Value:=-o;
    End;


    CDS.Post;
    ADOQuery1.Next;
  End;
  CDS.First;
end;


Я постарался дать подробные комментарии. Но, если у Вас возникнут вопросы, я жду их по адресу vs259@mail.ru.

Что остается? Остается только подложить вызов этой процедуры под кнопку:


procedure TOborotFrm.Button1Click(Sender: TObject);
begin
  CalcOborots(Date_N.Value,Date_K.Value);
end;



Но, смотреть на экране объемный документ не всегда удобно. Поэтому в следующих уроках я планирую рассказать, как экспортировать данные в Excel и как "прикрутить" к программе бумажный отчет.
















вторник, 6 марта 2012 г.

Урок 31. Работа с ресурсами. Создание компонентов - кнопок.



Доброго времени суток.

Чтобы продолжить создание программы "Расходы", мне понадобится пара кнопок.

Продвинутый программер Pascal/Delphi скажет, что кнопка есть на панели Standart, есть BitBtn на панели Additional, чего еще остается желать? Отвечу. Если Вы создаете одну программу, тогда можно пользоваться стандартными компонентами, наделяя их нужными свойствами. А мне по долгу службы иногда приходилось создавать по две-три формы в день, располагая на каждой по паре кнопок: "OK" и "Cancel". Ну и что, казалось бы? Но для каждой кнопки приходилось загружать рисунок, настраивать свойства привязки (якоря), убирать надпись и т.п. Ленивому как я человеку это быстро надоедает. И тогда на ум приходит идея создания собственных компонентов кнопок, свойства которых уже установлены в таком виде, в каком они чаще всего используются.


Итак, начнем.

Прежде всего, если у Вас установлена библиотека компонентов Pro Delphi Lib, деинсталлируйте ее.








Откройте любой графический редактор и нарисуйте изображения для двух кнопок.
Обращаю Ваше внимание, что цвет левого нижнего пикселя будет считаться прозрачным!



Этот файл я назвал Set_16_16.bmp.



Второй файл я назвал Cancel_16_16.bmp







Рисунки имеют размеры 16 на 32 пикселя. Обязательно сохраните их в формате bmp. Это важно.

Фактически, можно было бы обойтись одним изображением 16 на 16, как это уже реализовано в библиотеке Pro Delphi Lib, но одним рисунком можно обходиться, если кнопки всегда доступны. Если же свойство Enabled установить в False, то на месте кнопки появится неприглядный квадратик без надписи и какого бы ни было изображения. Это не есть гуд...

Далее, при помощи блокнота создаем файл OK_Cancel_New.rc (файл OK_Cancel без New не годится, такой уже был). Обратите внимание на расширение "rc".

В этом файле пишем две строки:

OK Bitmap "Set_16_16.bmp"
CANCEL Bitmap "Cancel_16_16.bmp"

где OK и CANCEL - названия (имена, без них как обратиться к нужному?) будущих ресурсов, Bitmap - тип ресурса и далее - имена подготовленных файлов с изображениями.

Сохраните этот файл.

В папку с этим файлом и файлами рисунков скопируйте файл brcc32.exe (его можно найти в папке с делфёй ($Delphi\bin)).

Облегчим себе жизнь. Тут я и позавидую и порадуюсь одновременно за тех, кто не работал в DOS-е с командной строкой... Создадим еще один файл в той же рабочей папке с помощью того же блокнота, но, чтобы он стал исполняемым операционной системой, дадим ему расширение "bat", например: runme.bat, следующего содержания:

brcc32.exe OK_Cancel_New.rc

Эта строка запустит программу brcc32.exe, которая в качестве параметра получит ссылку на файл OK_Cancel_New.rc (еще раз подчеркиваю, что все должно быть в одной папке, чтобы не прописывать абсолютных путей и не искать потом: куда же делся результат?).

В результате очень быстрого компилирования в рабочей папке появится так необходимый нам файл

OK_Cancel_New.RES

Посмотреть его содержимое можно разными программами для работы с ресурсами, но увидите Вы примерно вот такую картину:



Открываем оболочкой Delphi файл PrDButtons.pas, содержащийся в библиотеке Pro Delphi Lib.




Внесем в него необходимые изменения:
вместо строки

{$R *.res}

запишем новую директиву компиллятору - ссылку на созданный нами файл ресурсов:

{$R OK_Cancel_New.RES}

Общие свойства, как вы можете видеть, задаются для компонента TPrDBUTTON в его конструкторе (от этого компонента наследуются новые кнопки TPrDOkButton = class( TPrDBUTTON ) и TPrDCancelButton = class( TPrDBUTTON ) ):



constructor TPrDBUTTON.Create( AOwner: TComponent );
begin
  inherited Create(AOwner);
  Width:=26;
  Height:=26;
  ControlStyle := ControlStyle - [ csSetCaption ];
  ShowHint:=True;
  Anchors:=[akRight]+[akBottom];  // Изменение свойств привязки к правому нижнему углу
end;





Далее - можно добавить в published разделы для кнопок свойства:



  TPrDOkButton = class( TPrDBUTTON )
  public
    constructor Create( AOwner: TComponent ); override;
  published
    property Default default True;
    property ModalResult default mrOK;
    property NumGlyphs;                             
  end;


  TPrDCancelButton = class( TPrDBUTTON )
  public
    constructor Create( AOwner: TComponent ); override;
  published
    property Cancel default True;
    property ModalResult default mrCancel;

    property NumGlyphs;                             

  end;


А в конструкторе компонентов задать значение этого свойства, равное 2, т.к. на каждую кнопку теперь полагается два рисунка:



constructor TPrDOkButton.Create( AOwner: TComponent );
begin
  inherited Create(AOwner);
  Default:= True;
  ModalResult:= mrOK;
  Hint:='Кнопка подтверждения (ОК)';
  Glyph.LoadFromResourceName(HInstance, 'OK');
  NumGlyphs:=2;                        
end;


constructor TPrDCancelButton.Create( AOwner: TComponent );
begin
  inherited Create( AOwner );
  Cancel := True;
  ModalResult := mrCancel;
  Hint:='Кнопка отмены (Cancel)';
  Glyph.LoadFromResourceName(HInstance, 'CANCEL');
  NumGlyphs:=2;                        
end;


Установите вновь библиотеку компонентов Pro Delphi Lib.


Грех - не применить модернизированные компоненты на практике. Если Вы следите за разработкой программы "Расходы", то, возможно, обратили внимание на то, что после редактирования строки нет возможности ее сохранить...


Добавьте на верхнюю панельку пару кнопок.


А дальше - по проторенной ранее дорожке: нужно добавить в список действий новую операцию:


Data_OK (внимание: свойство Caption нужно оставить пустым):


В секции частных объявлений дать декларацию новой процедуры, которую необходимо указать в обработчике onExecute новой операции:



  private
    { Private declarations }
    procedure SetPost();


Процедура SetPost выполнит следующие действия:

перепишет введенные в элементы управления данные в поля таблицы и выполнит сохранение данных, т.е. - отправку их в источник, затем - вернет форму к исходному виду.


procedure TMainFrm.SetPost();
begin

  ADOTableMain.Edit;                                                // Перевод таблицы в режим редактирования
  ADOTableMain.FieldByName('MyDate').Value:=DataEdit.Value;         // Передача значений из
  ADOTableMain.FieldByName('D').Value:=DEdit.KeyValue;              // элементов управления формы
  ADOTableMain.FieldByName('K').Value:=KEdit.KeyValue;              // в поля таблицы
  ADOTableMain.FieldByName('Summa').Value:=SEdit.Value;
  ADOTableMain.FieldByName('Rem').Text:=PEdit.Text;

  Try
    ADOTableMain.Post;                                              // Попытка записи
  Except
    ADOTableMain.Cancel;                                            // Сброс в случае ошибки
    MyMessenger.TitleString:='Ошибка';                              // Сообщение оператору об ошибке
    MyMessenger.MessageString:='При попытке записать данные в базу возникла проблема.';
    MyMessenger.Buttons:=[mbOK];
    MyMessenger.ShowMessage;
  end;

  DBGridEh1.Enabled:=True;                                          // Переключение грида в режим доступности
  Panel3.Visible:=False;                                            // Скрытие верней панели

end;

Не забудьте для кнопки "OK" указать Action.

Удачи.
А.В.





среда, 22 февраля 2012 г.

Урок 30. Поиск и фильтрация

"Давненько я не брал в руки шашек...", - говорил Гоголевский Ноздрёв.

Давненько я ничего не писал в моем блоге. Не буду приносить никаких извинений и писать оправдательных фраз. Достаточно одного слова: "Работа..." А посему - за дело...

Вряд ли какая-то программа обходится без алгоритма поиска чего либо. А интерфейс, предоставляющий пользователю возможность работы с цифрами, тем более не может обойтись без таких полезных функций как поиск и фильтрация записей. Я утверждаю это со 100% уверенностью как пользователь собственных творений. А причина тому проста: чаще всего оператору не нужен полный набор всех записей. Как правило, его интересует ограниченное количество строк или вообще одна конкретная запись.

Если кто-то из Вас, мои дорогие читатели, работал с подобными программами, то мог сталкиваться с таким удобством: щелкнешь по заголовку какой либо колонки в таблице, и порядок сортировки строк таблицы изменился... Волшебство? Нет, уверяю Вас. Предлагаю снабдить сетку на главной форме подобными функциями. Это возможно, так как grid на форме main из библиотеки EhLib.

Не помню, описывал я ранее или нет, но для удобства пользователя (а об этом в очередной раз призываю не забывать никогда!) нужно установить такой порядок сортировки записей в главной форме при ее старте, чтобы самые свежие строки оказались вверху. Много для этого не нужно, укажите лишь в инспекторе объектов для ADOTableMain имя поля, по которому должна произойти сортировка, не забыв при этом указать обратный порядок (DESC):





Затем нужно обратиться к свойствам сетки:


SortLocal=True (по умолчанию стоит False).

Затем в событии onShow формы написать следующий обработчик:


procedure TMainFrm.FormShow(Sender: TObject);

Var
  i: Integer;
begin

  // Настройка заголовков колонок
  For i:=0 to DbGridEh1.Columns.Count-1 do begin
    DbGridEh1.Columns[i].Title.TitleButton := TRUE;                  //заголовок колонки становится "кнопкой"
  end;

// Настройка свойств сетки (добавляются свойства)
  DbGridEh1.OptionsEh := DbGridEh1.OptionsEh + [dghAutoSortMarking]; //автоматическая сортировка
  DbGridEh1.OptionsEh := DbGridEh1.OptionsEh + [dghMultiSortMarking]; //сортировка по нескольким колонкам

end;

А для клика по заголовку сетки создать обработчик вида:


procedure TMainFrm.DBGridEh1TitleBtnClick(Sender: TObject; ACol: Integer;

  Column: TColumnEh);
begin

If DBGridEh1.Columns[ACol].Title.SortMarker=smDownEh
then
  ADOTableMain.IndexFieldNames:=DBGridEh1.Columns[ACol].FieldName
else
  ADOTableMain.IndexFieldNames:=DBGridEh1.Columns[ACol].FieldName+' DESC';

end;,

здесь сначала проверяется, какой маркер сортировки установлен в колонке с номером ACol (напомню, что нумерация ведется от нуля), а затем назначается индекс по этой колонке для сортировки в прямом или обратном (DESC) порядке.

Если на этом этапе Вы скомпилируете проект, то все будет хорошо, пока Вы не щелкнете по заголовку поля "Дебет" или "Кредит". В этот момент произойдет исключительная ситуация,




связанная с тем, что эти поля в сетке не простые, а с подстановкой (на экране отображается содержимое поля D_Name, а на самом деле ключевую роль (KeyField) выполняет поле с именем D). Решений у этой задачки несколько, но я полагаю, что останавливаться на этом не стоит.

Ну, а чтобы все работало без ошибок пока Вы думаете, исключите обработку этих полей, написав в начале последней процедуры:

If (DBGridEh1.Columns[ACol].FieldName='D_Name') OR (DBGridEh1.Columns[ACol].FieldName='K_Name')
then
  exit;

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

Для этого понадобится текстовое поле, где будет вводиться искомое значение и кнопка. Назовем их FindEdit и BitBtnFind.

Напишите следующий обработчик события нажатия кнопки:

procedure TMainFrm.BitBtnFindClick(Sender: TObject);
begin

  // Поиск первого вхождения
  ADOTableMain.First;
  While not ADOTableMain.Eof do
  begin
    If Copy(ADOTableMain.FieldByName(DBGridEh1.Columns[DBGridEh1.SelectedIndex].FieldName).AsString,1,Length(FindEdit.Text))=FindEdit.Text
    then
      Break;
    ADOTableMain.Next;
  end;

  // Сообщение при неудачном поиске
  If ADOTableMain.eof
  then
  begin
    MyMessenger.MessageString:='Попробуйте: '
                              +CR
                              +'1. Изменить искомое слово.'
                              +CR
                              +'2. Проверить регистр.'
                              +CR
                              +'3. Установить курсор в одно из полей <Куда> или <Откуда>';
    MyMessenger.TitleString:='Ничего не найдено';
    MyMessenger.MessageType:=mtInformation;
    MyMessenger.Buttons:=[mbOK];

    MyMessenger.ShowMessage;

    // Установка фокуса в поле ввода
    If FindEdit.CanFocus
    then
      FindEdit.SetFocus;

  end;

end;

Самое интересное на мой взгляд место в этом коде - функция Copy, которая выделяет подстроку в поле, начиная с первой позиции. Длина этой подстроки берется равной длине введенного в поле поиска значения.

Для тестирования конструкции перейдите на колонку "Примечания" таблицы и введите несколько первых букв любого содержащегося в ней текста. Нажав на кнопку поиска, Вы переместите курсор на первое совпадающее значение.

Нужно ли при всем этом "изобретать" кнопку "Найти следующее..."? Думаю: "Нет!" Гораздо полезнее окажется кнопка ,устанавливающая фильтр по найденному значению. Т.е. все похожие записи будут представлены в таблице, а другие - временно скрыты.

Приступим к организации фильтра.

Прежде всего, создайте экшн в категории Data под данное действие (на предыдущем шаге создавать Action особого смысла не было, так как выполняемое по кнопке действие вряд ли имеет смысл подкладывать под какой-то другой элемент управления или пункт меню - действие связано с содержимым поля ввода и не работает само по себе).


Обработчик события onExecute напишите следующий:

procedure TMainFrm.Data_FilterExecute(Sender: TObject);
begin

  // Фильтр
  If MySelect.MySel_Field='MyDate'
  then
    NewFilter:='('+OldFilter+') AND (MyDate='+ADOTableMain.FieldByName('MyDate').AsString+')';

  If MySelect.MySel_Field='D_Name'
  then
    NewFilter:='('+OldFilter+') AND (D='+ADOTableMain.FieldByName('D').AsString+')';

  If MySelect.MySel_Field='K_Name'
  then
    NewFilter:='('+OldFilter+') AND (K='+ADOTableMain.FieldByName('K').AsString+')';

  If MySelect.MySel_Field='Summa'
  then
    NewFilter:='('+OldFilter+') AND (Summa='+ADOTableMain.FieldByName('Summa').AsString+')';

  ADOTableMain.Filtered:=False;
  ADOTableMain.Filter:=NewFilter;
  ADOTableMain.Filtered:=True;

  StatusBar1.Panels[0].Text:='Записей (фильтр): '+IntToStr(ADOTableMain.RecordCount);

end;

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

Установите этот Action под соответствующий пункт меню.
Тестирование будет состоять в следующем: 

расположите курсор в любой строке в колонке "Дата", воспользуйтесь фильтром... получите набор записей за эту дату, в строке состояния отобразится количество отфильтрованных записей. Переместите курсор на любую сумму и еще раз воспользуйтесь фильтром. Отфильтрованных записей станет меньше, возможно, останется только одна.

Но, вот беда: как вернуть все записи? Как снять фильтр? Как отказаться от него?

Понадобится еще одно действие в категории Data вот с таким обработчиком события "выполнить":

procedure TMainFrm.Data_NoFilterExecute(Sender: TObject);
begin

  // Снятие фильтра
  ADOTableMain.Filtered:=False;
  ADOTableMain.Filter:=OldFilter;
  ADOTableMain.Filtered:=True;
  StatusBarUpdate;

end;

Для отмены действия вновь наложенного фильтра назначьте это действие пункту меню или кнопке.

На этом на сегодня все.

Жду Ваших вопросов.



четверг, 26 января 2012 г.

Урок 29. Ударим кликом по гриду

В разработке программы "Расходы" (ссылку на исходные тексты и готовый exe файл я опубликую несколько позже) на уроке 28 я остановился на том, что показал один из возможных способов ввода информации в базу данных посредством интерфейса пользователя.

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

Я подозреваю, что нужно объяснить, для чего это нужно.

Предоставим пользователю одиночным кликом выбирать поле (колонку). Впоследствии этот выбор будет необходим, чтобы применить фильтр для отображения не всех записей выбранного мемориального ордера, а только тех записей, значение которых в данном поле совпадает с выбранным. Ну, например: для поиска ошибки ввода нам нужно посмотреть, сколько затрат в данном месяце отнесено на оплату коммунальных услуг. Подобные вопросы возникают довольно часто, если появляется сомнение: "Я внес эту сумму или нет?" или "Кажется, я уже вносил эту сумму..."

По двойному клику удобно вызвать процесс редактирования текущей записи (т.е. записи, по которой происходит двойной клик).

(рисунок можно увеличить).

Вряд ли стоит рассказывать или снимать видео о том, как задать обработчик того или иного события.

Я приведу здесь дописанный в модуле "main.pas" текст:

procedure TMainFrm.DBGridEh1CellClick(Column: TColumnEh);
begin

  // Клик по ячейке
  MySelect.MySel_Field:=DBGridEh1.Columns[DBGridEh1.SelectedIndex].FieldName;

end;

procedure TMainFrm.DBGridEh1DblClick(Sender: TObject);
begin

  // Двойной клик
  InsEdit;
end;

В первой процедуре в переменную типа "запись" MySelect.MySel_Field помещается значение поля выбранной мышкой колонки.

Вторая процедура вызывает процесс редактирования записи, описанный ранее.

Таким образом, собирая воедино фрагменты приводимого мною текста, Вы в итоге получите готовую полнофункциональную программу. Но, скажу честно, Вам придется набраться терпения, до финиша еще очень и очень далеко. Впереди у нас - отчетность (как текстовая, так и графическая) и многое другое.

А в следующий раз я планирую поговорить о поиске и фильтрации записей.



вторник, 24 января 2012 г.

Я вот, что подумал...

Коль скоро, это - мой блог, я могу иногда писать в нем на околопрограммистские темы.

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

Нет ничего проще.

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

Смотрите презентацию

пятница, 20 января 2012 г.

Добро пожаловать!

Я рад приветствовать моих постоянных читателей на blogger.com.

Сайт "Доступные видео уроки про Delphi" перестанет существовать с 17 марта 2012 года.
Причины, по которым я решил распроститься с доменом просты:

пусть и небольшие расходы на содержание сайта не окупились в течение года.

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

Кроме того, я не стал разбираться с причиной появления спам комментариев: виноват ли в этом сервер, предоставляющий хостинг или движок Вордпресс, мне не интересно. Отключив возможность комментирования, я фактически на 100% лишил себя обратной связи.

Мне было важно узнать Ваше мнение, уважаемые читатели, и я опубликовал опрос. Но, к сожалению, даже из сотни подписчиков откликнулись только 20%. Да и среди этого небольшого количества не нашлось ни одного, кто бы высказал конструктивные пожелания или критику.

Учитывая все это и многое другое, я принял решение продолжать вести тему написания программ и компонентов на языке Pascal (Delphi), но делать я это буду здесь, на сайте http://www.blogger.com под именем http://pro-delphi.blogspot.com/. Скорее всего, я отойду от принятой ранее формы уроков и видео уроков. И тематика уже не сайта, а блога, скорее всего, станет шире. Я, например, готов поделиться своими наработками в области программирования под OS Android или познаниями в использовании Adobe Photoshop. Думаю, что форма общения будет ближе к форме дневника программиста. Тем не менее, обещаю, что каждый найдет для себя в этом дневнике что-то полезное.

Спасибо.

Как говорит любимый мультяшный герой Карлсон: "Продолжаем разговор..."