Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Теллес М. - Borland C++ Builder. Библиотека программиста - 1998

.pdf
Скачиваний:
790
Добавлен:
13.08.2013
Размер:
4.35 Mб
Скачать

Borland C++ Builder (+CD). Библиотека программиста 361

Рис. 17.6. Страница 2 формы Мастера компонентов

Рис. 17.7. Страница 3 формы Мастера компонентов

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

void __fastcall TPagesDlg::FormCreate(TObject *Sender)

{

// Загружаем комбинированный список

Table1->First(); while ( !Table1->Eof )

{

ComboBox1->Items->Add(Table1->FieldValues["CLASSNAME"]); Table1->Next();

}

}

Borland C++ Builder (+CD). Библиотека программиста 362

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

void __fastcall TPagesDlg::ComboBox1Change(TObject *Sender)

{

// Получаем имя выбранного класса

Table1->Filter = "CLASSNAME = '" + ComboBox1->Text + "'"; Table1->Filtered = true;

//Переходим на эту запись

Table1->Last();

//Получаем идентификатор класса

AnsiString strClassId = Table1->FieldValues["ClassID"]; Table1->Filtered = false;

// Загружаем свойства

Table2->Filter = "CLASSID = '" + strClassId + "'"; Table2->Filtered = true;

Table2->First(); ListBox1->Clear(); while ( !Table2->Eof )

{

ListBox1->Items->Add( Table2->FieldValues["Method"] ); Table2->Next();

}

// Загружаем методы

Table3->Filter = "CLASSID = '" + strClassId + "'"; Table3->Filtered = true;

Table3->First(); ListBox3->Clear(); while ( !Table3->Eof )

{

ListBox3->Items->Add( Table3->FieldValues["PropertyNa"] ); Table3->Next();

}

}

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

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

кнопки будут использоваться для внесения свойств и замещения методов базового класса в наш класс. Во вторую страничку нашего диалога добавьте два обработчика для кнопок > и < (кнопки 3 и 4 формы):

Borland C++ Builder (+CD). Библиотека программиста 363

void __fastcall TPagesDlg::Button3Click(TObject *Sender)

{

// Если что-нибудь выбрано в первом списке if ( ListBox1->ItemIndex != -1 )

{

ListBox2->Items->Add( ListBox1-> Items->Strings[ListBox1->ItemIndex] ); ListBox1->Items->Delete( ListBox1->ItemIndex );

}

}

//--------------------------------------------------------

void __fastcall TPagesDlg::Button4Click(TObject *Sender)

{

// Если что-нибудь выбрано во втором списке if ( ListBox2->ItemIndex != -1 )

{

ListBox1->Items->Add( ListBox2-> Items->Strings[ListBox2->ItemIndex] ); ListBox2->Items->Delete( ListBox2->ItemIndex );

}

}

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

void __fastcall TPagesDlg::Button6Click(TObject *Sender)

{

//Если что-нибудь выбрано в первом списке if ( ListBox3->ItemIndex != -1 )

{

ListBox4->Items->Add( ListBox3-> Items->Strings[ListBox3->ItemIndex] ); ListBox3->Items->Delete( ListBox3->ItemIndex );

}

}

//------------------------------------------------------

void __fastcall TPagesDlg::Button7Click(TObject *Sender)

{

//Если что-нибудь выбрано во втором списке

if ( ListBox4->ItemIndex != -1 )

{

ListBox3->Items->Add( ListBox4-> Items->Strings[ListBox4->ItemIndex] ); ListBox4->Items->Delete( ListBox4->ItemIndex );

}

}

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

Borland C++ Builder (+CD). Библиотека программиста 364

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

Форма для определения новых методов

Первая форма, которую нам потребуется добавить в проект, — это форма для определения и добавления новых методов в компонент. Эта форма позволит пользовате лю определить название метода, тип возвращаемого значения, а также типы и названия аргументов. Вся эта информация будет скомбинирована в стандартное для C++ описание метода и добавлена в список методов, которые будут добавлены в класс при его генерации. Это позволит нам использовать один и тот же код во всех местах, где мы имеем дело с методами. На рис. 17.8 показана форма, которую мы будем использовать для описания новых методов.

Рис. 17.8. Форма для определения новых методов

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

void __fastcall TForm2::FormCreate(TObject *Sender)

{

StringGrid1->Cells[1][0] = "Field Type"; StringGrid1->Cells[2][0] = "Argument Name"; StringGrid1->ColWidths[0] = 10; StringGrid1->ColWidths[1] = 100; StringGrid1->ColWidths[2] = StringGrid1->Width-140;

}

Как видите, ничего сложного. Мы просто задаем значения для заголовков и ширины колонок, чтобы при запуске эта форма выглядела прилично. Единствен ное, пожалуй, неочевидное действие, которое надо проделать над этой формой, — это изменить свойства Options объекта StringGrid так, чтобы флаги goEditing и goTabs были установлены в значение true. Флаг goEditing позволяет пользователю редактировать данные прямо в ячейках сетки (мы это делаем для того, чтобы пользователь вообще смог ввести значения типов и названий), а установка флага goTabs позволяет использовать клавишу Tab для перемещения между ячейками сетки.

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

Borland C++ Builder (+CD). Библиотека программиста 365

д Include Unit Hdr. Ниже приведен код для обработчика нажатия на кнопку Новые страницы методов:

void __fastcall TPagesDlg::Button5Click(TObject *Sender)

{

if ( Form2->ShowModal() )

{

// Строим строку метода

AnsiString s = Form2->MethodReturn->Text; s += " ";

s += Form2->MethodName->Text; s += "( ";

//Аргументы boolean bFlag = false;

for ( int i=1; i<Form2->StringGrid1->RowCount; ++i )

{

AnsiString strArg = "";

//В колонке типов должен быть указан тип

if ( Form2->StringGrid1->Cells[1][i].Length() )

{

if ( bFlag ) s += ", ";

strArg += Form2->StringGrid1->Cells[1][i]; strArg += " ";

strArg += Form2->StringGrid1->Cells[2][i]; s += strArg;

bFlag = true;

}

}

s += ");";

// Добавляем в список

ListBox2->Items->Add( s );

}

}

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

Страница свойств

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

Borland C++ Builder (+CD). Библиотека программиста 366

Рис. 17.9. Форма для определения новых свойств

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

void __fastcall TPagesDlg::Button8Click(TObject *Sender)

{

if ( Form1->ShowModal() )

{

AnsiString s = Form1->PropertyType->Text; s += " ";

s += Form1->PropertyName->Text; ListBox4->Items->Add( s );

}

}

Форма отображается как модальное окно, в нее заносится информация о новом свойстве, при нажатии кнопки OK генерируется строка свойства, имеющая вид:

<тип-свойства> <название свойства>

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

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

Генерация исходного кода компонента

Когда пользователь нажимает кнопку OK страничного диалога, он подразумевает, что приложение в ответ на это сгенерирует ему код для нового компонента. Очевидно, что сначала программа должна провести некоторые проверки (например, проверить, введены ли имена исходного и заголовочного файла), но в основном она будет оставлять введенные вами значения на вашей совести, не пытаясь их исправлять. Если, например, вы задали недопустимые имена для свойств, программа позволит сгенерировать эти свойства. Это неизбежная расплата за объем работы по написанию приложения чем больше в программе проверок, тем труднее ее написать, а главное, отладить. Естественно, никто не собирается ограничивать свободу вашего творчества если вы захотите сделать программу более строгой пожалуйста. Если вы сможете довести ее до ума,

Borland C++ Builder (+CD). Библиотека программиста 367

дайте мне знать я с большим интересом посмотрю на результат.

Итак, добавьте обработчик для кнопки OK; добавьте в него следующий код:

void __fastcall TPagesDlg::OKBtnClick(TObject *Sender)

{

// Проверяем введенную информацию на полноту if ( ClassName->Text.Length() == 0 )

{

MessageBox(NULL,"Необходимо указать имя класса!","Error",MB_OK); return;

}

if ( SourceFile->Text.Length() == 0 )

{

MessageBox(NULL, "Необходимо задать имя исходного файла !", "Error", MB_OK ); return;

}

if ( HeaderFile->Text.Length() == 0 )

{

MessageBox(NULL, " Необходимо задать имя заголовочного файла!", "Error", MB_OK ); return;

}

// Создаем результирующий исходный файл

FILE *fp = fopen(HeaderFile->Text.c_str(), "w"); if ( fp == NULL )

{

MessageBox(NULL, " Не могу открыть заголовочный файл! ", "Error", MB_OK );

return;

}

GenerateHeaderFile(fp);

fclose(fp);

FILE *sfp = fopen(SourceFile->Text.c_str(), "w"); if ( sfp == NULL )

{

MessageBox(NULL, " Не могу открыть исходный файл!", "Error", MB_OK );

return;

}

GenerateSourceFile(sfp);

fclose(sfp);

}

Этот код сам по себе особо не примечателен, но на его примере я хотел бы дать вам пару советов. Во-первых, из этого кода видно, как можно прервать процесс генерации кода для компонента, выдав сообщение об ошибке (настоятельно рекомендую) и возвратив управление из обработчика. Во-вторых, в этом коде используются старомодные функции ввода-вывода fopen и fprintf. Так вот, эти функции чудесно работают в CBuilder, так что не бойтесь их использовать. Вы, конечно, вполне можете работать и с потоками ввода/вывода (stream), но в данном приложении широко используется форматированный вывод слабое место этих потоков.

Borland C++ Builder (+CD). Библиотека программиста 368

Сама функция проверяет, введены ли все необходимые для генерации поля (имя класса, базовый класс, заголовочный файл). Если это сделано, программа пытается открыть заголовочный файл для записи. Хочу отметить, что это решение вовсе не является очевидным. В данном случае я предпочел целиком переписывать при генерации исходный и заголовочный файлы, хотя более перспективным на первый взгляд вариантом было бы добавление кода в существующий файл. Однако в этом случае нас подстерегает куда больше проблем, чем видно с первого взгляда, особенно это касается заголовочного файла. Придется беспокоиться обо всех подключаемых файлах, особенно о файлах базового класса, и не только об этом. Чтобы избежать всех этих проблем, я принял более простой вариант каждый раз создавать новые файлы. Собственно, эта проблема не относится напрямую к основной теме нашего разговора; мне просто хотелось обратить ваше внимание на один из подводных камней, подстерегающих вас на пути разработчика компонентов и Мастеров, и продемонстрировать необходимость мыслить глобальными категориями, а не конкретными нуждами отдельной программы.

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

void TPagesDlg::GenerateHeaderFile(FILE *fp)

 

{

 

fprintf(fp,"//------------------------------------------------------- -------------------

\n");

fprintf(fp,"#ifndef %sH\n", ClassName->Text.c_str() );

 

fprintf(fp,"#define %sH\n", ClassName->Text.c_str() );

 

fprintf(fp,"//------------------------------------------------------- --------------------

\n");

fprintf(fp,"#include <vcl\SysUtils.hpp>\n");

 

fprintf(fp,"#include <vcl\Controls.hpp>\n");

 

fprintf(fp,"#include <vcl\Classes.hpp>\n");

 

fprintf(fp,"#include <vcl\Forms.hpp>\n");

 

fprintf(fp,"//------------------------------------------------------- --------------------

\n");

fprintf(fp,"class %s : public %s\n",

 

ClassName->Text.c_str(),

 

ComboBox1->Text.c_str() );

 

fprintf(fp,"{\n");

 

fprintf(fp,"private:\n");

 

GenerateMemberVariables(fp);

 

fprintf(fp,"protected:\n");

 

fprintf(fp,"public:\n");

 

for ( int i=0; i<ListBox2->Items->Count; ++i )

 

{

 

fprintf(fp, " %s\n",

 

ListBox2->Items->Strings[i].c_str() );

 

}

 

fprintf(fp,"__published:\n");

 

GenerateProperties(fp);

 

fprintf(fp,"};\n");

 

fprintf(fp,"//------------------------------------------------------- --------------------

\n");

fprintf(fp,"#endif\n");

 

}

 

Ничем не примечательная функция, не правда ли? Для генерации заголовочного файла нам требуется некоторая основа. В заголовочном файле всегда содержится пара ifdef/endif, которая

Borland C++ Builder (+CD). Библиотека программиста 369

предотвращает неоднократное включение заголовочного файла; для использования VCL включаются заголовочные файлы базового класса. После этого идет собственно описание класса. Оно состоит из выражения, определяющего класс, фигурных скобок, секций private, public и protected. Мы добрались до нашей первой проблемы генерации переменных-членов класса в секции private. Каждое свойство, определенное в классе, должно иметь соответствующую ему переменную класса для хранения данных.

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

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

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

void TPagesDlg::GenerateMemberVariables(FILE *fp)

{

//Ищем свойства, в описании которых есть пробел

//Этим свойствам надо сопоставить переменные класса for ( int i=0; i<ListBox4->Items->Count; ++i )

{

AnsiString s = ListBox4->Items->Strings[i]; if ( strchr( s.c_str(), ' ') )

{

char szProp[ 256 ]; char szType[ 256 ];

//Можем найти конец строки s += " ";

int nPos = GetWordQuoted( s.c_str(), szType, NULL); GetWordQuoted( s.c_str()+nPos, szProp, NULL);

//Создаем переменную класса этого типа

fprintf(fp, " %s F%s;\n", szType,

szProp );

}

}

}

Borland C++ Builder (+CD). Библиотека программиста 370

Для самих свойств мы вызываем метод GenerateProperties. Этот метод тоже выполняет различные действия в зависимости от того, является ли свойство подсаженным. Если да, то генерируется прототип свойства:

__property Fred;

С другой стороны, если свойство является новым, нам потребуется его полное описание: __property int Fred={read=FFred, write=FFred}

Вот как будет выглядеть полный код для метода GenerateProperties: void TPagesDlg::GenerateProperties(FILE *fp)

{

// Ищем свойства, в описании которых есть пробел for ( int i=0; i<ListBox4->Items->Count; ++i )

{

AnsiString s = ListBox4->Items->Strings[i]; if ( strchr( s.c_str(), ' ') )

{

char szString[ 256 ]; char szProp[ 256 ]; char szType[ 256 ];

strcpy ( szString, s.c_str() );

// So we can find the end of the string. strcat ( szString, " " );

int nPos = GetWordQuoted( szString, szType, NULL); GetWordQuoted( szString+nPos, szProp, NULL); fprintf(fp, " __property %s %s={read=F%s, write=F%s};\n",

szType,

szProp,

szProp, szProp );

}

else

fprintf(fp, " __property %s;\n", s.c_str());

}

}

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

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

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

void TPagesDlg::GenerateSourceFile(FILE *fp)

 

{

 

fprintf(fp,"//------------------------------------------------------- --------------------

\n");

fprintf(fp,"#include <vcl\\vcl.h>\n");

 

fprintf(fp,"#pragma hdrstop\n\n");

 

fprintf(fp,"#include \"%s\"\n", HeaderFile->Text.c_str());

 

Соседние файлы в предмете Программирование на C++