пятница, 23 марта 2012 г.

Урок 38. Обработчик ошибок

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

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

Первый вопрос, который я предвижу от слабо посвященных: "А зачем?"
Действительно, пишешь задачу, как "честная кнопка", пытаешься ничего не упустить, все предусмотреть, собственное творчество, как правило, себе нравится, и не вызывает сомнений правильность его алгоритма... И все же! Если задачка небольшая - все просто и прозрачно. Но, если алгоритм сложен... как говорят "глаз замыливается", перестает находить узкие места. К тому же накладывается человеческий фактор: откуда Вам было знать, что пользователь не введет значение в поле и нажмет "Enter"?.. В этот момент Ваша программа напугает его каким-нибудь англоязычным сообщением да еще и с пиктограммой "Error"... А, если Вы помните, я с самого начала просил Вас уважать пользователя и заказчика. Стоит ли доводить ситуацию до общения с плохо вменяемым пиплом?! Если Вы не хотите общаться с человеком, у которого от испуга вытаращены глаза и трясутся руки, нужно научиться предугадывать (перехватывать) ошибки и выдавать на экран сообщения на родном для пользователя языке, желательно с номером Вашего телефона... Это и есть часть понятия "дружественный интерфейс".

Следующий вопрос, к которому я хочу приблизить тебя, дорогой читатель, это вопрос:

где и когда нужен обработчик ошибок?

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

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

Чем большим количеством подобных вопросов Вы себя озадачите, тем меньше вероятность того, что Ваша программа в руках незадачливого пользователя перейдет в состояние "Не отвечает".

Наиболее общая схема обработчика ошибок выглядит примерно так:

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

Добавьте в объявление переменных в процедуре SetLang() еще парочку:

Var
  ini: TIniFile;
  ErrKod: integer;
  ErrMsg: String;

а так же - объявление метки:

Label
  ErrLabel;

и стартовые значения:


begin

  ErrKod:=0;                                    // Это значение при отсутствии ошибок
  ErrMsg:= 'Операция завершена успешно';

Настройте заранее (в начале процедуры) компонент, выдающий сообщения:


  MyMessenger.TitleString:='Ошибка...';
  MyMessenger.MessageType:=mtError;
  MyMessenger.Buttons:=[mbOK];


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


  If Not FileExists(GetCurrentDir + '\'+Lang)
  then
  begin
    ErrKod:=1;
    ErrMsg:='При попытке открытия файла языковой настройки произошла ошибка';
  end;


Почему именно здесь нужно предусмотреть какие-то действия? А вдруг в файле настроек указан несуществующий файл? Или его кто-то с диска того... нечаянно стер...

Следом должна идти проверка на наличие ошибок:


  if ErrKod>0
  then
    Goto ErrLabel;


...


Если в результате этой проверки выясняется, что код ошибки стал больше нуля (а проверок может быть несколько и код ошибки может принимать десяток разных значений), то оператор безусловного перехода, минуя основную ветвь алгоритма, отправит нас на метку ErrLabel, после которой и написан нехитрый в данном случае обработчик ошибок:


...


ErrLabel:


    MyMessenger.MessageString:=ErrMsg;
    MyMessenger.ShowMessage;


end;        // Процедуры


Давайте нашалим: переименуем файл Rus.lng, например в Rus1.lng, чтоб программа его уж точно не нашла.

Тогда при старте Rashod.exe будет произведена попытка настроить интерфейс приложения с помощью указанного в файле настроек Rashod.ini, в секции [Constants], в переменной LangFileName файла Rus.lng, которого в рабочем каталоге нет. Сработает обработчик ошибки, и на экране будет выведено придуманное нами сообщение:


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

Верните переименованному файлу его законное название и запустите программу еще раз.
Вы получите еще одно сообщение:


не корректное по оформлению и неуместное по сути (если ошибок нет, нужно ли в данном случае вообще выводить какое-то сообщение?!).

Давайте поправим ситуацию:

ErrLabel:

  Case ErrKod of
    0:                               // Ошибок нет
    else
    begin
      MyMessenger.MessageString:=ErrMsg;
      MyMessenger.ShowMessage;
    end;

  End;

end;        // Процедуры


Теперь в отсутствии ошибок программа загрузится в штатном режиме, т.е. без лишних сообщений.

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





А знаете, кто написал первый компьютерный вирус?

Ваш покорный слуга.

Шучу, конечно.

Дело было во времена фортрана.

У нас на кафедре было заведено так: сочинил программу к курсовому или еще по какому торжественному случаю - набивай её на перфокартах. Колоду перфокарт упакуй аккуратненько, чтоб не рассыпалась (не приведи Господи!), подпиши разборчиво, положи ее в специальный шкафчик и иди гуляй, потому что когдааа-а-а оператор вычислительного зала возьмет ее своими бесценными ручками и запустит в считыватель M4030, не известно... К тому же, вдруг кто-нибудь совсем безрукий по неосторожности опять паяльник разогретый в процессор уронит... Тогда - совсем плохо...

Короче: "Приходите завтра" - это не название фильма а ваш девиз с того памятного момента и до бесконечности...

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

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

На это и был основной расчет: в каждой программе переменная I объявлена, значит ей можно пользоваться... вот вам и уязвимость...

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

I=0
Label1: I=I+1
GOTO Label1

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

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

Далеко разносился вопль негодования по гулким коридорам высшей школы и затихал где-то под прокуренной лестницей...

Значит: сработало...

А скоро - первое апреля...





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

Урок 37. Локализация интерфейса

Наверное, не правильно говорить о переводе всех названий пунктов меню и кнопок на другие языки, если Вы пишете программу для использования в домашнем хозяйстве. Да, это так, если не учитывать, что на страницах этого сайта я преследую несколько иную цель: не дать исходники, а дать "know how" - умения.

Я уже где-то говорил, и не устану повторять:  для решения той или иной задачи существует несколько способов. Вдаваться в полемику в пользу того или иного мне не интересно. Лишь обозначу, что для решения задачи локализации можно использовать библиотеку, т.е. Dll-ку, а можно - файл настроек. Обязательно найдутся охотники по ратовать за использование реестра Windows. Это, еще раз повторю - дело вкуса. В каждом из способов есть свои достоинства и свои недостатки.

Известно, что файл настроек может иметь вот такую структуру:

[Название секции]
Имя переменной1=Значение переменной1
Имя переменной2=Значение переменной2
...
Имя переменнойN=Значение переменнойN


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

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

Чтобы файлы языковой поддержки не путались с файлами настроек (ini), предлагаю дать им расширение *.lng.

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

Rus.lng


[MainFrm]
MainFormCap=Расходы (главная форма)
Menu_File=&Файл
Menu_File_Exit=&Выход
Menu_Data=&Данные
Menu_Data_Edit=&Ввести
Menu_Data_Insert=&Новая запись
Menu_Data_Filter=&Фильтр
Menu_Data_Credit=&Кредиты
Menu_Data_Export=&Экспорт
Menu_Data_Import=&Импорт
Menu_Reports=&Отчетные формы
Menu_Reports_Reverse_statement=&Оборотная ведомость
Menu_Reports_Turnovers_account=&Обороты по счету
Menu_Reports_Cost_Analysis_All=&Анализ затрат (все)
Menu_Reports_Cost_Analysis_Gruops=Анализ затрат (по &группам)
Menu_Reports_Cost_Analysis_Trends=Анализ &тенденций
Menu_Codifiers=&Кодификаторы
Menu_Codifiers_Accounts=&Счета
Menu_Codifiers_Group_costs=&Группы затрат
Menu_Codifiers_Memorial_warrants=&Мемориальные ордера
Menu_Codifiers_Valuta=&Валюты
Menu_Service=&Настройки
Menu_Service_SelectDisk=Выбор &диска (пути к базе данных)
Menu_Service_Language=&Выбор языка
Menu_Service_Clear=&Очистка БД
Menu_Help=&Справка
Menu_Help_About=&О программе


Eng.lng


[MainFrm]
MainFormCap=Expenses (Main form)
Menu_File=&File
Menu_File_Exit=&Exit
Menu_Data=&Data
Menu_Data_Edit=&Edit
Menu_Data_Insert=&New Record
Menu_Data_Filter=&Filter
Menu_Data_Credit=&Credits
Menu_Data_Export=&Export
Menu_Data_Import=&Import
Menu_Reports=&Reports
Menu_Reports_Reverse_statement=&Reverse statement
Menu_Reports_Turnovers_account=&Turnovers account
Menu_Reports_Cost_Analysis_All=&Cost analysis (All)
Menu_Reports_Cost_Analysis_Gruops=Cost analysis (&gruops)
Menu_Reports_Cost_Analysis_Trends=Check &Trends
Menu_Codifiers=&Codifiers
Menu_Codifiers_Accounts=&Accounts
Menu_Codifiers_Group_costs=&Group costs
Menu_Codifiers_Memorial_warrants=&Memorial warrants
Menu_Codifiers_Valuta=&Valuta
Menu_Service=&Service
Menu_Service_SelectDisk=&Select path
Menu_Service_Language=&Language
Menu_Service_Clear=&Clear data
Menu_Help=&Help
Menu_Help_About=&About prog

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

Кстати, в Rashod.ini должна пополниться секция:

[Constants]
LangFileName=Rus.lng

- пусть уж программа стартует с привычного нам языка, чтоб самих себя не испугать :-)

На этом предварительные танцы с бубнами и беличьими хвостиками закончены. Открываем проект "Rashod".

И первое, что необходимо сделать - создать процедуру, устанавливающую локализацию, назову ее SetLang (не забудьте декларировать ее в секции private):

procedure TMainFrm.SetLang();
Var
  ini: TIniFile;
begin

  // Настройка интерфейса программы (по выбранному языку)
  ini:=TiniFile.Create(GetCurrentDir + '\'+Lang);

  MainFrm.Caption:=Ini.ReadString('MainFrm','MainFormCap','');
  N_File.Caption:=Ini.ReadString('MainFrm','Menu_File','');
  N_Exit.Caption:=Ini.ReadString('MainFrm','Menu_File_Exit','');
  N_Data.Caption:=Ini.ReadString('MainFrm','Menu_Data','');
  N_Edit.Caption:=Ini.ReadString('MainFrm','Menu_Data_Edit','');
  N12_Ins.Caption:=Ini.ReadString('MainFrm','Menu_Data_Insert','');
  N10_Filter.Caption:=Ini.ReadString('MainFrm','Menu_Data_Filter','');
  N_Credit.Caption:=Ini.ReadString('MainFrm','Menu_Data_Credit','');
  N_ExportToMDB.Caption:=Ini.ReadString('MainFrm','Menu_Data_Export','');
  N_Import.Caption:=Ini.ReadString('MainFrm','Menu_Data_Import','');
  N_Reports.Caption:=Ini.ReadString('MainFrm','Menu_Reports','');
  N3.Caption:=Ini.ReadString('MainFrm','Menu_Reports_Reverse_statement','');
  N4.Caption:=Ini.ReadString('MainFrm','Menu_Reports_Turnovers_account','');
  N_Analiz.Caption:=Ini.ReadString('MainFrm','Menu_Reports_Cost_Analysis_All','');
  N_AnalizGroup.Caption:=Ini.ReadString('MainFrm','Menu_Reports_Cost_Analysis_Gruops','');
  N_Analiz_Trends.Caption:=Ini.ReadString('MainFrm','Menu_Reports_Cost_Analysis_Trends','');
  N_Kodifikators.Caption:=Ini.ReadString('MainFrm','Menu_Codifiers','');
  N1.Caption:=Ini.ReadString('MainFrm','Menu_Codifiers_Accounts','');
  N_ExpGr.Caption:=Ini.ReadString('MainFrm','Menu_Codifiers_Group_costs','');
  N2.Caption:=Ini.ReadString('MainFrm','Menu_Codifiers_Memorial_warrants','');
  N_Val.Caption:=Ini.ReadString('MainFrm','Menu_Codifiers_Valuta','');
  N_Serv.Caption:=Ini.ReadString('MainFrm','Menu_Service','');
  N7_SelectDisk.Caption:=Ini.ReadString('MainFrm','Menu_Service_SelectDisk','');
  N_Serv_SelLang.Caption:=Ini.ReadString('MainFrm','Menu_Service_Language','');
  N12_ServClear.Caption:=Ini.ReadString('MainFrm','Menu_Service_Clear','');
  N_Help.Caption:=Ini.ReadString('MainFrm','Menu_Help','');
  N_About.Caption:=Ini.ReadString('MainFrm','Menu_Help_About','');

  ini.Free;

end;

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

Необходимая переменная Lang, хранящая имя языкового файла, должна быть объявлена в секции interface модуля формы. А вот значение в нее попадет в момент создания формы (Create), откуда - из Rashod.ini:

procedure TMainFrm.FormCreate(Sender: TObject);
var
  ini: TIniFile;
begin

  // Подключение и считывание из файла настроек ini
  ini:=TiniFile.Create(GetCurrentDir + '\Rashod.ini');
  Lang:=Ini.ReadString('Constants','LangFileName','');
  ini.Free;

  SetLang;
...

- И что? - скажете Вы после компиляции проекта, - никаких изменений нет.
Видимых изменений нет. Попробуйте поменять в Rashod.ini Rus.lng на Eng.lng и еще раз запустить программу.

Можно радоваться? 
Рано.

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

Для этого нужно придумать обработку соответствующего пункта меню (Настройки - Выбор языка):

procedure TMainFrm.N_Serv_SelLangClick(Sender: TObject);  // Настройка. Выбор языка
var
  ini: TIniFile;
begin

  // Настройка фильтра файлов определенного типа в окне диалога
  OpenDialog1.Filter:='Файлы языковой поддержки|*.lng';

  // Выбор файла на диске
  If not OpenDialog1.Execute
  then                           // Отмена выбора
  begin
    exit;
  end;

  // Извлекается
  Lang:=OpenDialog1.FileName;    // Полный путь
  Lang:=ExtractFileName(Lang);   // Только имя файла

  // Запись в соответствующую секцию Rashod.ini, чтобы
  // в следующий раз программа стартовала с этими настройками
  ini:=TiniFile.Create(GetCurrentDir + '\Rashod.ini');
  Ini.WriteString('Constants','LangFileName',Lang);
  ini.Free;

  // Смена надписей на элементах управления
  SetLang;

end;

В комментариях я постарался дать необходимые пояснения. Вот теперь программа "Rashod" еще больше стала похожа на "взрослую" :-)

Я не затрагиваю так или иначе вопросы оптимизации уже написанного кода - это отдельная непростая история. Но здесь меня просто подмывает сделать оговорку:

вместо двух строк

  Lang:=OpenDialog1.FileName;    // Полный путь
  Lang:=ExtractFileName(Lang);   // Только имя файла

можно написать одну

  Lang:=ExtractFileName(OpenDialog1.FileName);

Иногда приходится жертвовать красотой ради наглядности :-)

Если остались вопросы - пожалуйте на страницу "Обратная связь"







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

Урок 36. Продолжаем разговор...

Именно так любил приговаривать очаровательный мультяшный герой по имени Карслон, который живет на крыше...

Прежде, чем углубиться дальше в лес Delphi и не только,  я бы хотел оживить некоторые пункты меню проекта "Расходы".

Шаг 1. Пункт меню "О программе"


Когда-то, в уроке №13, я рассказал о том, как сделать компонент "О программе". Вам придется его немного подправить :-) и разместить на форме MainFrm, а затем назначить соответствующему пункту меню вот такой простой обработчик:


procedure TMainFrm.N_AboutClick(Sender: TObject);
begin
  PrDAboutBoxDlg1.Execute;
end;


Наделите компонент следующими свойствами:



Щаг 2. Никто не обратил внимания, что кнопка Cancel (PrDCancelButton1) на верхней панельке осталась без обработчика. Добавьте обработку действий, когда пользователь ничего не ввел в поля новой записи или передумал добавлять новую запись:


procedure TMainFrm.PrDCancelButton1Click(Sender: TObject);
begin


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


end;

Шаг 3. Новый Action Data_Insert, обрабатывающий процесс добавления новой записи, необходимо добавить в список ActionList и подложить его под соответствующие пункты главного меню и PopupMenu:


Обработчик события выполнения экшена:


procedure TMainFrm.Data_InsertExecute(Sender: TObject);
begin


  // Новая запись
  ADOTableMain.Insert;


end;



Внимание! Не забыть вернуться к процедуре:

procedure TMainFrm.Data_SelectMOExecute(Sender: TObject);    // Соответствует N_EditClick
begin
...
  //   Сделать доступным именно Action, а пункты меню станут доступными автоматически
  Data_Insert.Enabled:=True;


Шаг 4. Выбор базы данных

Добавьте Action в категорию Service


Создайте обработчик Action:




procedure TMainFrm.Service_Select_DiskExecute(Sender: TObject);
begin


  // Выбор диска с базой данных
  SetDisk;
  MainConnecting;
  TableActive(True);


  // Инфо на статусбаре
  StatusBarUpdate;


end;

Установите соответствие пункта меню и нового действия.
Данный пункт меню позволит Вам выбрать для просмотра и работы другую базу данных (например, по прошествии года, после архивации базы данных, сохранения ее в другом каталоге).

На этом пока все.
В следующем уроке я предполагаю показать как "перевести" интерфейс программы на другой язык.



понедельник, 19 марта 2012 г.

Урок 35. Оборотная ведомость. Печать отчета

Если Вы, изучив предыдущие уроки, получили в свое распоряжение файл Excel, Вам не составит труда вывести его на печать. Но, не всегда такой вариант получения отчета приемлем.

Для конструирования отчетов существует ряд средств. Я не берусь здесь обсуждать их достоинства и недостатки. Так исторически сложилось, что я пользуюсь платной версией Fast Report.

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

Почему именно FastReport? Посмотрите здесь. Не только из гордости за отечественного производителя, еще и за практически безграничные возможности.

Установка компонентов FastReport не должна вызвать никаких трудностей. После установки, на Tool Palette появится ряд вкладок.

Разместите на форме два компонента, с вкладки FastReport:

TfrxDBDataset - источник данных для отчета, TfrxReport - конструктор отчета.

Теперь необходимо немного подготовиться и временно включить:

1. Форма MainFrm: ADOConnection  - Connected = True (если не задана строка подключения, задать ее).
2. Форма OborotFrm, компонент CDS - свойство Connection = MainFrm.ADOConnection1 (т.е. связать этот компонент с ADOConnection  на главной форме) и указать свойство TableName = Oborot. Собственно, это все проделывается программно, но нам нужно временно для создания отчета.

Не забудьте по окончании создания отчета очистить значения этих свойств.

Настройте компонент TfrxDBDataset, указав его свойство DataSet:

А затем два раза щелкните по компоненту TfrxReport, чтобы перейти в конструктор отчета.
Первое, что необходимо сделать - выбрать меню Report - Data и назначить источник данных для отчета, как показано на рисунке:


Затем - развернуть страницу на Landscape (альбомную) в меню File - Page Settings, т.к. отчет наш "широкий".

Добавить "бэнды" - так называются области для размещения в них различных данных и группировке этих данных, как минимум - два - ReportTitle (заголовок отчета) и MasterData (мастер - для вывода данных отчета), связав последний с источником данных.


Сохраните свой макет под именем Oborot.fr3.

Теперь настало время дать отчету заголовок, нажав на пиктограмму "ab":



Форматируйте надпись подобно тому, как Вы это делали в MS WORD, здесь инструменты очень похожи:


Перетащите одно поле для начала на MasterData (можно добавить еще заголовок страницы (PageHeader), где разместить заголовки колонок отчета):


Опция Create caption удобна, но не в нашей языковой зоне: подписи = имена полей на латинице, так что подписи придется сделать ручками.

Сохраните и закройте конструктор. Теперь нужно в Popup меню формы добавить новый пункт, по которому будет вызываться отчет:


procedure TOborotFrm.N2Click(Sender: TObject);
begin
  // Вывод предварительного просмотра отчета
  frxReport1.ShowReport(True);
end;

Если все сделано правильно, на этом этапе Вы получите список счетов с заголовком "Оборотная ведомость".

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


Главное рассказал, а важное и нужное - еще впереди.
Мой дорогой читатель, наверное уже догадался, что в документе не хватает подзаголовка, т.е. периода, за который он составлен.

Далее я покажу один из способов передачи данных посредством переменных в отчет.

Добавьте перед строкой  frxReport1.ShowReport(True) ровно такую же строку, какой задавался период при импорте отчета в Excel:

frxReport1.Script.Variables['Period']:='Период: '+FormatDateTime('dd/mm/yy',Date_N.Value)+' - '+FormatDateTime('dd/mm/yy',Date_K.Value);  
здесь Period - имя переменной, которую нужно разместить в конструкторе отчета в текстовом поле (memo) в квадратных скобках:


Результат, прямо скажем еще далек от совершенства:


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

Если не сможете разобраться - пишите (координаты Вы найдете через страницу "Обо мне"), и тогда будем разбираться вместе.

А.В.






пятница, 16 марта 2012 г.

Урок 34. Создание библиотеки (Dll)

"...когда-то давно, когда наша Земля была еще маленькая 
и тепленькая, и по ней бегали такие ма-а-а-аленькие...
и с тех пор пошло и пошло, пошло и пошло..."

Мой отец коллекционировал записи А.Райкина, Р.Карцева и В.Ильченко, М.Мироновой и А.Менакера и других... Я любил слушать их вместе с ним, поэтому помню многое наизусть...

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

К чему я это все? Да к тому, что застав в своем развитии вычислительную машину ДВК-2М, я приобрел привычку экономно относиться к памяти. Кто из Вас сейчас, владеющих гигабайтными дисками, способен представить себе пятидюймовую дискету стандартным объемом 360 килобайт? При особом искусстве разметить (форматировать) эту дискету можно было на 720кБ. И, о счастье!, в ДВК было целых два дисковода! Значит, в распоряжении программиста - аж целых 1440 килобайт! Не тут-то было! А операционная система? Значит, на пространстве немногим более одного (!) мегабайта нужно было исхитриться и прожить. Но эта трудность была даже не главной. А крошечное ОЗУ? Вот где была настоящая засада! Вот куда нужно было умудриться запихнуть еще что-то помимо операционной системы! Собственно от этого и пошло то, о чем я сегодня поведу речь...

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

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

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

Совсем другое дело, если у Вас много наработок. Тем более, если десяток программ используют одни и те же функции. Привести пример? Легко: перевод числа в строку прописью, подсчет доходности по ценным бумагам... в нашем случае - экспорт отчета в Excel.

Да-да, именно этот функционал я и предлагаю перетащить в библиотеку, вдруг где-то еще понадобится?

Начать следует с создания модуля библиотеки:

Много вопросов визард не задаст, но сгенерирует вот такое предупреждение:


library Project1;


{ Important note about DLL memory management: ShareMem must be the
  first unit in your library's USES clause AND your project's (select
  Project-View Source) USES clause if your DLL exports any procedures or
  functions that pass strings as parameters or function results. This
  applies to all strings passed to and from your DLL--even those that
  are nested in records and classes. ShareMem is the interface unit to
  the BORLNDMM.DLL shared memory manager, which must be deployed along
  with your DLL. To avoid using BORLNDMM.DLL, pass string information
  using PChar or ShortString parameters. }


uses
  SysUtils,
  Classes;


{$R *.res}


begin
end.


В двух словах: если функции Вашей библиотеки будут передавать или получать строковые значения, то нужно в Uses на первом месте иметь ссылку на ShareMem, соответственно, в системе должна присутствовать библиотека BORLNDMM.DLL. 

Если же, использование этой библиотеки по каким-то причинам невозможно (а такое бывает!), то передаваемые параметры должны быть типов PChar or ShortString.


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

Сохраним проект под именем ProDelphiLyb.

Добавим в Uses ссылки на следующие модули:

  ShareMem,          // Нас предупреждали!!!

  DBGridEh,           // Для работы с сеткой
  ExcelXP,            // Для работы с приложением Excel
  DB,                 // Для работы с закладкой


и далее - текст процедуры, практически полностью совпадающий с аналогичной процедурой из предыдущего урока:


Procedure MyExportToExcel(WorkSheetName: String;  PGridEh: TDBGridEh;  XLApp: TExcelApplication; RepName: String; RepPeriod: String); stdcall;


// Вывод данных из источника объекта DBGridEh в Excel


var
  WorkBk: _WorkBook;     //  определяем WorkBook
  WorkSheet: _WorkSheet; //  определяем WorkSheet
  I, J: Integer;
  SavePlace: TBookmark;


begin




Try


  // Установка закладки
  SavePlace := PGridEh.DataSource.DataSet.GetBookmark;


  // Соединяемся с сервером TExcelApplication
  XLApp.Connect;


  // Добавляем WorkBooks в ExcelApplication
  XLApp.WorkBooks.Add(xlWBatWorkSheet, 0);


  // Выбираем первую WorkBook
  WorkBk := XLApp.WorkBooks.Item[1];


  // Определяем первый WorkSheet
  WorkSheet := WorkBk.WorkSheets.Get_Item(1) as _WorkSheet;


  // Заполняем свойства WorkSheet
  WorkSheet.Name := WorkSheetName;


  // Заголовок отчета
  J := 1;
  Worksheet.Cells.Item[J,1].Value:= 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:= RepPeriod;
  Worksheet.Cells.Item[J,1].Font.Italic := True;


  // Заголовки колонок
  J := 3;
  PGridEh.DataSource.DataSet.First;
  with 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 PGridEh.DataSource.DataSet do
    begin
      First;
      while not Eof do
        begin
          with 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
  XLApp.Visible[0] := True;


  // Разрываем связь с сервером
  XLApp.Disconnect;


  // Возврат на закладку
  PGridEh.DataSource.DataSet.GotoBookmark(SavePlace);
  PGridEh.DataSource.DataSet.FreeBookmark(SavePlace);


Finally
End;
end;


exports
MyExportToExcel



begin
end.                                 // NB точка в конце

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

Выполните Compile и Build проекта. В результате последнего действия, в каталоге с проектом появится файл: ProDelphiLyb.dll

Теперь - самое время вернуться к проекту "Расходы".



Найдите в модуле главной формы процедуру ExportTo и замените все ее содержимое (которое переехало в модуль library ProDelphiLyb)  вот на это:


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


  // Экспорт в Эксель
  Try
    MyExportToExcel(p_WorkSheetName, p_PGridEh, p_XLApp, p_RepName, p_RepSubTitle);
  Except
    MyMessenger.TitleString:='Проблема';
    MyMessenger.MessageString:='Возможно, в Вашем компьютере не установлен MS Excel';
    MyMessenger.Buttons:=[mbOk];
    MyMessenger.ShowMessage;
  end;


end;


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


implementation
Uses  MOs, Oborot;


{$R *.dfm}


Procedure MyExportToExcel(WorkSheetName: String;  PGridEh: TDBGridEh;  XLApp: TExcelApplication;
                          RepName: String; RepPeriod: String); stdcall; external 'ProDelphiLyb.dll';



Если все сделано правильно, то работа программы "Расходы" внешне ни чем не будет отличаться от того, как она работала до сего момента. Но, если Вы попробуете установить эту программу на другой компьютер, у Вас, вполне вероятно, будут неприятности, связанные с использованием BORLNDMM.DLL, точнее - с отсутствием этой библиотеки в компьютере.

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

начать нужно с модуля библиотеки, убрав из Uses Sharemem и изменив тип принимаемых параметров со String на PChar:


library ProDelphiLyb;
uses
//  ShareMem,
  DBGridEh,           // Для работы с сеткой
  ExcelXP,            // Для работы с приложением Excel
  DB,                 // Для работы с закладкой
  SysUtils,
  Classes;


{$R *.res}


Procedure MyExportToExcel(WorkSheetName: PChar;  PGridEh: TDBGridEh;  XLApp: TExcelApplication; RepName: PChar; RepPeriod: PChar); stdcall;


далее - в теле самой процедуры выполнить преобразование типов, поскольку ячейки Excel, в которые вписываются заголовок отчета и подзаголовок (период) не могут принять значения типа PChar, им - строки подавай:


  Worksheet.Cells.Item[J,1].Value:= StrPas(RepName);
и
  Worksheet.Cells.Item[J,1].Value:= StrPas(RepPeriod);


Save, compile, build.

В модуле main проекта "Расходы" в объявлениях процедур тоже внести необходимые поправки:


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


(не забудьте внести аналогичные поправки и в декларации этой процедуры в разделе implementation !!!)

и


{$R *.dfm}


Procedure MyExportToExcel(WorkSheetName: PChar;  PGridEh: TDBGridEh;  XLApp: TExcelApplication; RepName: PChar; RepPeriod: PChar); stdcall; external 'ProDelphiLyb.dll';


И последнее, в модуле Oborot выполнить преобразование передаваемых параметров из строковых в PChar:

procedure TOborotFrm.N_ExportClick(Sender: TObject);
begin

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

end;


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







среда, 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 файла, похожего на этот:


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


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