Журнал LinuxFormat - перейти на главную

LXF91:Mono

Материал из Linuxformat
(Различия между версиями)
Перейти к: навигация, поиск
(викификация, оформление)
 
(Mono: Создаем ''GTK''-приложение=: викификация, оформление)
 
Строка 5: Строка 5:
 
{{Цикл/Mono}}
 
{{Цикл/Mono}}
  
=Mono: Создаем ''GTK''-приложение==
+
=Mono: Создаем ''GTK''-приложение=
  
 
: При вашем знании ''Mono'' обычные текстовые программы писать уже скучно. '''Пол Хадсон''' поможет разобраться с новым проектом, использующим графический интерфейс.
 
: При вашем знании ''Mono'' обычные текстовые программы писать уже скучно. '''Пол Хадсон''' поможет разобраться с новым проектом, использующим графический интерфейс.
 +
 +
Мы дошли всего до пятого урока, а мне уже не терпится показать вам магию ''GTK#''... Вообще-то я бы и погодил с
 +
показом, но столько читателей просили меня об этом – не
 +
игнорировать же народные массы! Надеюсь, вам понравились эксперименты с ''Mono'' – ведь это очень весело. Но, по моему мнению, стоит влезть в библиотеку ''GTK'', как начнутся первые проблемы. Видите ли,
 +
''GTK'' писали люди, не любившие принципы объектно-ориентированного программирования, но попытавшиеся реализовать их на ''С''. Да еще он безнадежно усложнен и почти непознаваем для новичка. Но вам
 +
повезло: я рядом!
 +
 +
На первых четырех уроках вы создали программы '''Hello World''',
 +
утилиту для поиска файлов, RSS-агрегатор и простой интерфейс для
 +
''Beagle''. На этом уроке, чтобы целиком сконцентрироваться на ''GTK'', мы
 +
возьмем готовый код из RSS-проекта и создадим графический RSS-агрегатор, который я назову ''Chomp''. ''GTK'' не столько сложен, сколько муторен – если вы что-то не вполне поняли, вернитесь назад на пару
 +
абзацев: нить рассуждений потерять очень легко.
 +
 +
==Удар от Stetic==
 +
 +
Как я уже сказал, ''GTK'' написан на ''С'', но к нему существует интерфейс ''Mono/C#'', вполне предсказуемо названный ''GTK#''. Это очень тонкая обертка вокруг кода ''С'', а значит, работать с ней довольно нудно. Но одну часть нудной никак не назовешь: это ''Stetic'' – инструмент для построения графических интерфейсов пользователя в стиле
 +
'''Drag&Drop''', входящий в состав ''MonoDevelop''. Если вы помните первый
 +
урок данной серии, мой дистрибутив – Fedora Core 6, и те, кто тоже
 +
его установил, уже могут использовать ''MonoDevelop'' со ''Stetic''. Если
 +
у вас другой дистрибутив, убедитесь, что ''MonoDevelop'' в нем версии
 +
0.12 или старше.
 +
 +
{{Врезка
 +
|Заголовок=Stetic против Glade
 +
|Содержание=''Stetic'' и ''Glade'' очень похожи: оба поддерживают '''drag&drop''' для создания пользовательских интерфейсов на ''GTK''. Но ''Stetic'' интегрирован в ''MonoDevelop'', то есть может генерировать код, связывая виджеты с
 +
переменными. При желании можно использовать в ''Mono'' и ''Glade'', но рекомендуется все-таки ''Stetic''.
 +
|Ширина=200px
 +
}}
 +
 +
Первым делом нарисуем наш интерфейс с помощью ''Stetic''. Никакой
 +
функциональности это не даст, зато даст представление о том, как
 +
будет выглядеть готовое приложение, а я всегда считал, что это позволяет шагнуть далеко вперед. Другая причина, почему стоит сначала
 +
спроектировать интерфейс – вы сразу же увидите, будет ли ваш проект
 +
неуклюж или невразумителен, и, в случае чего, заранее примете меры
 +
по редизайну интерфейса. Помните, что у лучших проектов интерфейс
 +
пользователя разрабатывается очень тщательно – не зацикливайтесь
 +
исключительно на написании кода в надежде, что GUI сложится само
 +
собой!
 +
 +
Прежде чем приступать, полностью обновите версию Fedora, потому что ''Stetic'' – сравнительно молодой инструмент, и лучше использовать свежую версию ''MonoDevelop'' для минимизации числа возможных
 +
отказов. Да почаще сохраняйте результаты, поскольку ''Stetic'' не страдает излишней стабильностью.
 +
 +
Итак, аккуратно выполните инструкции по работе в ''MonoDevelop'',
 +
приведенные далее и продолжите чтение.
 +
 +
==Часть 1 Создание интерфейса пользователя==
 +
 +
[[Изображение:LXF92_momo01.png|Шаг 1]]
 +
 +
'''1  Создайте новый проект'''
 +
Выберите проект ''С#'' из области шаблонов, а
 +
затем – '''Gtk# 2.0 Project''', справа. Назовите его
 +
''chomp'', снимите галочку с '''Create Separate Solution Subdirectory''' и нажмите '''New'''.
 +
 +
[[Изображение:LXF92_momo02.png|Шаг 2]]
 +
 +
'''2  Откройте MainWindow'''
 +
Откройте дерево пользовательского интерфейса в
 +
панели '''Solution''' и дважды щелкните на '''MainWindow'''.
 +
Откроется '''MainWindow.cs''' в режиме '''Designer'''.
 +
 +
[[Изображение:LXF92_momo03.png|Шаг 3]]
 +
 +
'''3  Добавьте четыре области'''
 +
В палитре '''Widgets''' пройдите до '''Containers''' и перетащите '''VBox''' на окно. Щелкните на нем правой кнопкой и выберите '''Vbox1 > Insert Before для четырех областей'''.
 +
 +
[[Изображение:LXF92_momo04.png|Шаг 4]]
 +
 +
'''4  Преобразуйте области'''
 +
Перетащите главное меню, панель инструментов,
 +
'''Hpaned '''и строку состояния во все четыре области,
 +
начиная с верхней. Разделите '''Hpaned''' на две примерно равные части, как показано на рисунке.
 +
 +
[[Изображение:LXF92_momo05.png|Шаг 5]]
 +
 +
'''5  Новый Notebook'''
 +
Перетащите '''VBox''' в левую часть '''Hpaned'''. Нажмите
 +
на ней и выберите '''Delete'''. Перетащите '''Notebook''' и
 +
'''Button''' на верхнюю и нижнюю стороны '''VBox'''.
 +
 +
[[Изображение:LXF92_momo06.png|Шаг 6]]
 +
 +
'''6  TreeView и Calendar'''
 +
Перетащите '''Tree View''' на вкладку '''Page1''' компонента
 +
'''Notebook'''. Щелкните на нем правой кнопкой, выберите '''Notebook1''', затем '''Insert Page After'''. На вкладке
 +
'''Page2''' вставьте '''Calendar'''.
 +
 +
[[Изображение:LXF92_momo07.png|Шаг 7]]
 +
 +
'''7  Добавьте полосы прокрутки'''
 +
В большом пространстве справа от элемента
 +
'''Hpaned''' вставьте '''Scrolled Window''', затем поместите
 +
в него '''Text View'''. Пользователи смогут прокручивать текст.
 +
 +
[[Изображение:LXF92_momo08.png|Шаг 8]]
 +
 +
'''8  Организуйте меню'''
 +
Дважды щелкните на пустом меню, наберите '''/‘File’''', затем нажмите '''Enter'''. Таким образом, вы создадите
 +
подменю – наберите в нем '''‘Quit’'''. Создайте меню
 +
'''Help''' с подменю '''About'''.
 +
 +
[[Изображение:LXF92_momo09.png|Шаг 9]]
 +
 +
'''9  Кнопки панели инструментов'''
 +
Дважды щелкните по панели инструментов, выберите '''Select Icon''', затем '''gtk-new'''. Добавьте две другие
 +
кнопки: одну с '''gtk-refresh''', другую с '''gtk-delete'''.
 +
 +
==Часть 2 Реализуем функционал программы==
 +
 +
Создание интерфейса позади, но для готового GUI еще кое-чего нехватает. Нужно сделать три вещи. Во-первых, назначить нормальные имена виджетам: пока что они называются '''button1''', '''textview2''' и т.д. Во-вторых, связать виджеты с полями: тогда мы сможем обращаться к ним из кода на ''C#''. И в-третьих, изменить свойства виджетов, чтобы они
 +
выглядели как надо и делали то, что мы хотим.
 +
 +
{{Врезка
 +
|Заголовок=Скорая помощь
 +
|Содержание=Проверьте, что файл '''sitelist.txt''' содержит адреса URL лент, по одному на строке, и что он находится там же,
 +
где и файл '''chomp.exe'''. Обычно это каталог '''bin/Debug''' или '''bin/Release''', если вы сделали релиз-версию.
 +
|Ширина=200px
 +
}}
 +
 +
Имя виджета – это просто одно из свойств, поэтому задачи 1 и 3
 +
выполняются через панель '''Widget Properties''', находящейся в правом
 +
нижнем углу окна '''MonoDevelop'''. Вы увидите кнопку '''Bind To Field''' в верху
 +
окна, которое только создали – я вам скажу, когда она понадобится.
 +
 +
Вот список требуемых изменений:
 +
* Нажмите на кнопку '''New''' на панели инструментов '''toolbar''' (не на самой панели!) и измените ее имя на '''btnNew'''.
 +
* Измените имя кнопки '''Refresh''' на '''btnRefresh'''.
 +
* Измените имя кнопки '''Delete''' на '''btnDelete'''.
 +
* Измените имя '''Tree View''' на '''tvFeeds'''. Нажмите '''Bind To Field'''.
 +
* Снимите галочку с '''Show Day Names''' для '''Calendar''' (это его немного уменьшит). Нажмите '''Bind To Field'''.
 +
* Измените имя '''Text View''' на '''txtFeed'''. Нажмите '''Bind To Field'''. Измените '''Hight Request''' на 300, а '''Width Request''' на 400 (теперь '''Text View''' не будет занимать весь экран).
 +
* Измените имя кнопки под компонентом '''notebook''' на '''btnRefreshAll'''.
 +
* Измените текст для страниц компонента '''notebook''' на '''By Feed''' для вкладки с '''Tree View''' и на '''By Date''' для вкладки с '''Calendar'''.
 +
 +
==Добавим реализацию==
 +
 +
Вот и все – с интерфейсом пользователя покончено, можно сфокусироваться на реализации методов. Наберем кое-какой код прямо в
 +
файле '''MainWindow.cs''', а также установим обработчики сигналов для
 +
'''MainWindow.cs''' в ''Stetic''. Сигналы – это внутренние ''GTK''-сообщения,
 +
которые посылаются, когда что-нибудь происходит: например, пользователь нажимает кнопку или клавишу. По умолчанию, большая часть
 +
сигналов ничего не делает, но мы можем на них подписаться, назначив каждому обработчик с помощью ''Stetic'' – он даже создаст за нас базовый метод!
 +
 +
{{Врезка
 +
|Заголовок=Скорая помощь
 +
|Содержание=''GTK'' предусматривает масштабирование компонентов интерфейса при изменении размера окна, а также их сжатие и растяжение при переводе надписей на другие языки, смене шрифта и других параметров среды выполнения. Вот почему мы используем '''VBox''' и '''HBox'', а не размещаем компоненты вручную.
 +
|Ширина=200px
 +
}}
 +
 +
Прежде всего обработаем событие, возникающее, когда пользователь выделяет что-то в '''Tree View''' нашей программы. Этот компонент будет использоваться для отображения списка RSS-лент, на которые
 +
подписан пользователь, поэтому при выборе пользователем ленты из
 +
списка ''Chomp'' надо скачать последнюю версию RSS-файла и отобразить его в '''Text View''' справа. Щелчки пользователя можно обработать, отлавливая сигнал '''RowActivated''' – щелкните на '''Tree View''', затем на
 +
панели '''Widget Properties''' перейдите на вкладку '''Signals'''. Найдите сигнал
 +
'''RowActivated''' и дважды щелкните там, где написано '''Click Here To Add A New Handler'''. Назовите его '''OnRowActivated''' и нажмите '''Enter'''. Щелкнув
 +
на кнопке '''Source Code''', расположенной ниже окна дизайна формы, вы
 +
увидите, что ''Stetic'' создал следующий метод:
 +
 +
<source lang=csharp>
 +
protected virtual void OnRowActivated(object o, Gtk.
 +
RowActivatedArgs args)
 +
{
 +
}
 +
</source>
 +
 +
Вроде неплохо, но тут вы и подошли к первому недостатку ''GTK''.
 +
А именно: получить данные из '''Tree View''' гораздо сложнее, чем вы
 +
думаете!
 +
 +
''Chomp ''требуется работать с лентами двумя способами. Если мы
 +
находимся на вкладке '''By Feed''', то двойной щелчок на одной из лент
 +
в '''Tree View''' должен загрузить эту самую ленту. Если мы находимся
 +
на вкладке '''By Date''', при двойном щелчке на дате в календаре следует
 +
загрузить все ленты, но показать новости только за указанную дату.
 +
Чтобы как-то унифицировать наш код, создадим метод '''ReadFeed()''',
 +
который загружает URL, а затем фильтрует их по дате. При загрузке всех записей данной ленты мы передадим специальный аргумент
 +
'''DateTime.MinValue''', обозначающий «игнорировать дату».
 +
 +
Вот код, который надо поместить в '''OnRowActivated''':
 +
 +
<source lang=csharp>
 +
TreeSelection select = tvFeeds.Selection;
 +
TreeIter iter;
 +
TreeModel model;
 +
select.GetSelected(out model, out iter);
 +
string val = (string)model.GetValue(iter, 0);
 +
txtFeed.Buffer.Clear();
 +
ReadFeed(val, DateTime.MinValue);
 +
</source>
 +
 +
Первые пять строк иллюстрируют мои слова о занудстве ''GTK'': таким
 +
манером мы извлекаем значения из компонента '''Tree View'''. Весьма запутанно, но зато годится для любого возможного способа использования '''Tree View'''.
 +
 +
Когда выбранное значение попадет в переменную '''val''', мы очищаем
 +
буфер '''Text View''' и вызываем '''ReadFeed()''', чтобы загрузить и отобразить
 +
RSS. Но давайте сначала разберемся с календарем. Для этого найдите
 +
сигнал '''DaySelectedDoubleClick''' и назначьте ему обработчик '''DayClicked'''.
 +
В режиме '''Source Code''' среды ''MonoDevelop'' должен появиться пустой
 +
метод '''DayClicked()''', который выглядит так:
 +
 +
<source lang=csharp>
 +
protected virtual void DayClicked(object sender, System.
 +
EventArgs e)
 +
{
 +
}
 +
</source>
 +
 +
Этот метод отлавливает двойные щелчки на календаре, а получить
 +
выбранную дату из компонента '''Calendar''' очень просто.
 +
 +
Но не спешите хвалить ''GTK'': вот я вам сейчас покажу, как перебрать
 +
все элементы в '''Tree View'''. В '''DayClicked()''' надо поместить следующий
 +
код:
 +
 +
<source lang=csharp>
 +
txtFeed.Buffer.Clear();
 +
tvFeeds.Model.Foreach(FeedByDate);
 +
</source>
 +
 +
Здесь мы очищаем текстовый буфер для входящих лент, а затем
 +
вызываем метод '''Foreach()''' модели данных '''Tree View'''. Метод '''FeedByDate()'''
 +
(его я еще не показывал – не гоните коней!) вызывается для каждого
 +
элемента в '''Tree View'''. А так как метод '''FeedByDate()''' вызывается методом
 +
'''Foreach()''', он должен соответствовать конкретной функции-прототипу,
 +
то есть принимать определенный список параметров. Иначе он работать не будет.
 +
 +
Мы хотим, чтобы функция '''FeedByDate()''' читала каждую ленту, которую ей передают, затем посылала ее в '''ReadFeed()''' вместе с датой, определенной на компоненте '''Calendar'''. Это довольно просто:
 +
 +
<source lang=csharp>
 +
bool FeedByDate(TreeModel model, TreePath path, TreeIter iter)
 +
{
 +
string url = (string)model.GetValue(iter, 0);
 +
ReadFeed(url, new DateTime(calendar.Year, calendar.Month + 1,
 +
calendar.Day));
 +
return false;
 +
}
 +
</source>
 +
 +
Первая строка выцарапывает URL ленты из '''Tree View''', после чего
 +
он передается методу '''ReadFeed()''' вместе с годом, месяцем и днем,
 +
выбранными в календаре. Но вы заметили '''calendar.Month + 1'''? ''GTK''
 +
ведет отсчет месяцев с нуля. Зато дни и года он считает с 1. Я тоже не
 +
знаю, почему!
 +
 +
Пару выпусков назад мы рассматривали код, необходимый для
 +
чтения RSS-лент. На сей раз мы его используем, но с небольшими
 +
изменениями:
 +
 +
* Вывод будет происходить в текстовый буфер, а не в консоль.
 +
* GUID’ы кэшироваться не будут – мы хотим, чтобы ''Chomp'' загружала все записи каждой ленты, а не записи, которые мы не видели.
 +
* Если для времени указано '''DateTime.MinValue''', имеется в виду «по дате фильтровать не надо».
 +
* Иначе, получить дату публикации каждой новости и преобразовать ее в объект '''DateTime'''.
 +
* Затем сравнить каждый объект '''DateTime''' с указанной датой и вывести только те записи, которые ей соответствуют.
 +
 +
Тут есть мелкий просчет: одни ленты, включая '''Linux Format''', предоставляют дату публикации в формате '''Wed, 24 Jan 2007 11:02:43 +0000''', а другие вместо '''‘+0000’''' ставят '''‘GMT’'''. '''.NET''' понимает оба формата, но в ''Mono'' – возможно, по недосмотру – поддерживается только последний. В качестве обходного пути мы будем обрезать всю подстроку, начиная с символа '''+'''.
 +
 +
==Время писать код==
 +
 +
{{Врезка
 +
|Заголовок=Скорая помощь
 +
|Содержание=Когда вы будете читать этот материал, уже, возможно, выйдет ''MonoDevelop'' версии 1.0, хотя бы в виде
 +
бета-версии. ''Stetic'' – одна из активно разрабатываемых и быстро меняющихся областей, поэтому в новом релизе будут
 +
исправлены многие ошибки и даже добавлены новые... возможности. Стоит проверить!
 +
|Ширина=200px
 +
}}
 +
 +
Вот и код. Если вы читали [[LXF89:Mono|LXF89]], то должны узнать его части!
 +
 +
<source lang=csharp>
 +
  protected virtual void ReadFeed(string feed, DateTime filter) {
 +
    XmlDocument doc = new XmlDocument();
 +
    doc.Load(feed);
 +
    TextBuffer text = txtFeed.Buffer;
 +
    XmlNodeList items = doc.SelectNodes(“//item”);
 +
    foreach (XmlNode item in items) {
 +
      if (filter == DateTime.MinValue) {
 +
        text.Text += (item.SelectSingleNode(“title”).InnerText) + “\n”;
 +
        text.Text += (“ “ + item.SelectSingleNode(“description”).
 +
InnerText) + “\n\n”;
 +
      } else {
 +
        string time = item.SelectSingleNode(“pubDate”).InnerText;
 +
        if (time.Contains(“+”)) time = time.Substring(0, time.
 +
  IndexOf(“+”));
 +
        DateTime thisdate = DateTime.Parse(time);
 +
        if (filter.Day == thisdate.Day && filter.Month == thisdate.Month
 +
  && filter.Year == thisdate.Year) {
 +
          text.Text += (item.SelectSingleNode(“title”).InnerText) + “\n”;
 +
          text.Text += (“ “ + item.SelectSingleNode(“description”).
 +
InnerText) + “\n\n”;
 +
        }
 +
      }
 +
    }
 +
  }
 +
</source>
 +
 +
Две строки могут вызвать затруднение:
 +
 +
<source lang=csharp>
 +
  if (time.Contains(“+”)) time = time.Substring(0, time.IndexOf(“+”));
 +
  DateTime thisdate = DateTime.Parse(time);
 +
</source>
 +
 +
Первая означает «если строка содержит '''+''', просьба записать в переменную time все от начала строки (от символа с номером ноль) до знака '''+'''»: таким образом отсекается часть '''+0000'''. Вторая строка означает
 +
«возьми строку '''time'' и преобразуй ее в тип '''DateTime'''.» Хранить дату как
 +
'''DateTime''' удобнее, потому что можно работать с днем, месяцем и годом
 +
как с числами, а не разбирать строку вручную.
 +
 +
==Финишная прямая==
 +
 +
Первая версия ''Chomp'' почти завершена, но в ней кое-чего не хватает.
 +
Двойной щелчок по лентам загружает все ленты. Двойной щелчок по
 +
дате загружает все ленты и фильтрует записи по выбранной дате. Но в
 +
какой момент мы загружаем ленты в программу? Ответ: а ни в какой!
 +
 +
Наш предыдущий XML-считыватель тоже загружал список лент,
 +
поэтому можно просто взять и модифицировать его для нашего интерфейса на ''GTK''. А именно, давайте поместим каждый URL в '''Tree View''',
 +
чтобы пользователи могли нажимать на них. Здесь опять проявится
 +
занудство ''GTK'', поэтому возьмите себя в руки:
 +
 +
<source lang=csharp>
 +
  string[] sitelist;
 +
  if (File.Exists(“sitelist.txt”)) {
 +
    sitelist = File.ReadAllLines(“sitelist.txt”);
 +
  } else {
 +
    sitelist = new string[0];
 +
  }
 +
  // это значит, что мы хотим сохранить строку в строке Tree View, но в
 +
действительности нам нужен только URL!
 +
TreeStore store = new TreeStore (typeof(string), typeof(string));
 +
foreach(string site in sitelist) {
 +
  // “Foo” это место размещения
 +
  store.AppendValues(site, “Foo”);
 +
}
 +
tvFeeds.Model = store;
 +
tvFeeds.AppendColumn(“URLs”, new CellRendererText(), “text”,0);
 +
</source>
 +
 +
Чтобы показать данные, Tree View нужен по крайней мере один
 +
столбец. Смело отключайте заголовки в свойствах компонента ''Stetic'', в
 +
них нет нужды. Теперь создайте файл '''sitelist.txt''' в вашем рабочем каталоге программы (наподобие '''/путь/до/проекта/bin/Debug'''), заполните его URL-адресами лент, и пусть поработает!
 +
 +
Весь код этого учебника, даже с некоторыми дополнительными
 +
возможностями (см. врезку Chomp 0.2), включен на диск. Но чтобы проект созрел для помещения на '''SourceForge''', не мешает его усовершенствовать. Оставляю ''Chomp'' в ваших умелых руках... '''LXF'''
 +
 +
==Chomp 0.2==
 +
 +
Версия Chomp на диске базируется на том, что
 +
мы сделали, и позволяет пользователям временно добавлять новые ленты, но вы можете
 +
сделать еще кое-что:
 +
* Заставить работать функцию удаления ленты.
 +
* Кэшировать ленты и GUID, чтобы при отображении они загружались из памяти, а не скачивались каждый раз из Сети.
 +
* Реализовать поиск в кэшированных лентах.
 +
* Добавить '''Refresh''' и '''RefreshAll'''.
 +
* Поместить что-нибудь в строку состояния!
 +
* Заполнить меню осмысленными действиями.
 +
* Добавлять пользовательские ленты в файл '''sitelist.txt'''.
 +
 +
Целью этого урока было научить вас использовать ''Stetic'', и я думаю, мы справились неплохо. Дальнейшая работа будет в основном писа ниной кода: обвешаем интерфейс всякими крутыми штуками. Почаще сохраняйте результат, и все будет в порядке – резвитесь!

Текущая версия на 10:43, 28 ноября 2008


Mono-Maния Программирование на современной платформе для начинающих


Содержание

[править] Mono: Создаем GTK-приложение

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

Мы дошли всего до пятого урока, а мне уже не терпится показать вам магию GTK#... Вообще-то я бы и погодил с показом, но столько читателей просили меня об этом – не игнорировать же народные массы! Надеюсь, вам понравились эксперименты с Mono – ведь это очень весело. Но, по моему мнению, стоит влезть в библиотеку GTK, как начнутся первые проблемы. Видите ли, GTK писали люди, не любившие принципы объектно-ориентированного программирования, но попытавшиеся реализовать их на С. Да еще он безнадежно усложнен и почти непознаваем для новичка. Но вам повезло: я рядом!

На первых четырех уроках вы создали программы Hello World, утилиту для поиска файлов, RSS-агрегатор и простой интерфейс для Beagle. На этом уроке, чтобы целиком сконцентрироваться на GTK, мы возьмем готовый код из RSS-проекта и создадим графический RSS-агрегатор, который я назову Chomp. GTK не столько сложен, сколько муторен – если вы что-то не вполне поняли, вернитесь назад на пару абзацев: нить рассуждений потерять очень легко.

[править] Удар от Stetic

Как я уже сказал, GTK написан на С, но к нему существует интерфейс Mono/C#, вполне предсказуемо названный GTK#. Это очень тонкая обертка вокруг кода С, а значит, работать с ней довольно нудно. Но одну часть нудной никак не назовешь: это Stetic – инструмент для построения графических интерфейсов пользователя в стиле Drag&Drop, входящий в состав MonoDevelop. Если вы помните первый урок данной серии, мой дистрибутив – Fedora Core 6, и те, кто тоже его установил, уже могут использовать MonoDevelop со Stetic. Если у вас другой дистрибутив, убедитесь, что MonoDevelop в нем версии 0.12 или старше.


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

Прежде чем приступать, полностью обновите версию Fedora, потому что Stetic – сравнительно молодой инструмент, и лучше использовать свежую версию MonoDevelop для минимизации числа возможных отказов. Да почаще сохраняйте результаты, поскольку Stetic не страдает излишней стабильностью.

Итак, аккуратно выполните инструкции по работе в MonoDevelop, приведенные далее и продолжите чтение.

[править] Часть 1 Создание интерфейса пользователя

Шаг 1

1 Создайте новый проект Выберите проект С# из области шаблонов, а затем – Gtk# 2.0 Project, справа. Назовите его chomp, снимите галочку с Create Separate Solution Subdirectory и нажмите New.

Шаг 2

2 Откройте MainWindow Откройте дерево пользовательского интерфейса в панели Solution и дважды щелкните на MainWindow. Откроется MainWindow.cs в режиме Designer.

Шаг 3

3 Добавьте четыре области В палитре Widgets пройдите до Containers и перетащите VBox на окно. Щелкните на нем правой кнопкой и выберите Vbox1 > Insert Before для четырех областей.

Шаг 4

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

Шаг 5

5 Новый Notebook Перетащите VBox в левую часть Hpaned. Нажмите на ней и выберите Delete. Перетащите Notebook и Button на верхнюю и нижнюю стороны VBox.

Шаг 6

6 TreeView и Calendar Перетащите Tree View на вкладку Page1 компонента Notebook. Щелкните на нем правой кнопкой, выберите Notebook1, затем Insert Page After. На вкладке Page2 вставьте Calendar.

Шаг 7

7 Добавьте полосы прокрутки В большом пространстве справа от элемента Hpaned вставьте Scrolled Window, затем поместите в него Text View. Пользователи смогут прокручивать текст.

Шаг 8

8 Организуйте меню Дважды щелкните на пустом меню, наберите /‘File’, затем нажмите Enter. Таким образом, вы создадите подменю – наберите в нем ‘Quit’. Создайте меню Help с подменю About.

Шаг 9

9 Кнопки панели инструментов Дважды щелкните по панели инструментов, выберите Select Icon, затем gtk-new. Добавьте две другие кнопки: одну с gtk-refresh, другую с gtk-delete.

[править] Часть 2 Реализуем функционал программы

Создание интерфейса позади, но для готового GUI еще кое-чего нехватает. Нужно сделать три вещи. Во-первых, назначить нормальные имена виджетам: пока что они называются button1, textview2 и т.д. Во-вторых, связать виджеты с полями: тогда мы сможем обращаться к ним из кода на C#. И в-третьих, изменить свойства виджетов, чтобы они выглядели как надо и делали то, что мы хотим.


Имя виджета – это просто одно из свойств, поэтому задачи 1 и 3 выполняются через панель Widget Properties, находящейся в правом нижнем углу окна MonoDevelop. Вы увидите кнопку Bind To Field в верху окна, которое только создали – я вам скажу, когда она понадобится.

Вот список требуемых изменений:
  • Нажмите на кнопку New на панели инструментов toolbar (не на самой панели!) и измените ее имя на btnNew.
  • Измените имя кнопки Refresh на btnRefresh.
  • Измените имя кнопки Delete на btnDelete.
  • Измените имя Tree View на tvFeeds. Нажмите Bind To Field.
  • Снимите галочку с Show Day Names для Calendar (это его немного уменьшит). Нажмите Bind To Field.
  • Измените имя Text View на txtFeed. Нажмите Bind To Field. Измените Hight Request на 300, а Width Request на 400 (теперь Text View не будет занимать весь экран).
  • Измените имя кнопки под компонентом notebook на btnRefreshAll.
  • Измените текст для страниц компонента notebook на By Feed для вкладки с Tree View и на By Date для вкладки с Calendar.

[править] Добавим реализацию

Вот и все – с интерфейсом пользователя покончено, можно сфокусироваться на реализации методов. Наберем кое-какой код прямо в файле MainWindow.cs, а также установим обработчики сигналов для MainWindow.cs в Stetic. Сигналы – это внутренние GTK-сообщения, которые посылаются, когда что-нибудь происходит: например, пользователь нажимает кнопку или клавишу. По умолчанию, большая часть сигналов ничего не делает, но мы можем на них подписаться, назначив каждому обработчик с помощью Stetic – он даже создаст за нас базовый метод!


Прежде всего обработаем событие, возникающее, когда пользователь выделяет что-то в Tree View нашей программы. Этот компонент будет использоваться для отображения списка RSS-лент, на которые подписан пользователь, поэтому при выборе пользователем ленты из списка Chomp надо скачать последнюю версию RSS-файла и отобразить его в Text View справа. Щелчки пользователя можно обработать, отлавливая сигнал RowActivated – щелкните на Tree View, затем на панели Widget Properties перейдите на вкладку Signals. Найдите сигнал RowActivated и дважды щелкните там, где написано Click Here To Add A New Handler. Назовите его OnRowActivated и нажмите Enter. Щелкнув на кнопке Source Code, расположенной ниже окна дизайна формы, вы увидите, что Stetic создал следующий метод:

 protected virtual void OnRowActivated(object o, Gtk.
 RowActivatedArgs args)
 {
 }

Вроде неплохо, но тут вы и подошли к первому недостатку GTK. А именно: получить данные из Tree View гораздо сложнее, чем вы думаете!

Chomp требуется работать с лентами двумя способами. Если мы находимся на вкладке By Feed, то двойной щелчок на одной из лент в Tree View должен загрузить эту самую ленту. Если мы находимся на вкладке By Date, при двойном щелчке на дате в календаре следует загрузить все ленты, но показать новости только за указанную дату. Чтобы как-то унифицировать наш код, создадим метод ReadFeed(), который загружает URL, а затем фильтрует их по дате. При загрузке всех записей данной ленты мы передадим специальный аргумент DateTime.MinValue, обозначающий «игнорировать дату».

Вот код, который надо поместить в OnRowActivated:

 TreeSelection select = tvFeeds.Selection;
 TreeIter iter;
 TreeModel model;
 select.GetSelected(out model, out iter);
 string val = (string)model.GetValue(iter, 0);
 txtFeed.Buffer.Clear();
 ReadFeed(val, DateTime.MinValue);

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

Когда выбранное значение попадет в переменную val, мы очищаем буфер Text View и вызываем ReadFeed(), чтобы загрузить и отобразить RSS. Но давайте сначала разберемся с календарем. Для этого найдите сигнал DaySelectedDoubleClick и назначьте ему обработчик DayClicked. В режиме Source Code среды MonoDevelop должен появиться пустой метод DayClicked(), который выглядит так:

 protected virtual void DayClicked(object sender, System.
 EventArgs e)
 {
 }

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

Но не спешите хвалить GTK: вот я вам сейчас покажу, как перебрать все элементы в Tree View. В DayClicked() надо поместить следующий код:

 txtFeed.Buffer.Clear();
 tvFeeds.Model.Foreach(FeedByDate);

Здесь мы очищаем текстовый буфер для входящих лент, а затем вызываем метод Foreach() модели данных Tree View. Метод FeedByDate() (его я еще не показывал – не гоните коней!) вызывается для каждого элемента в Tree View. А так как метод FeedByDate() вызывается методом Foreach(), он должен соответствовать конкретной функции-прототипу, то есть принимать определенный список параметров. Иначе он работать не будет.

Мы хотим, чтобы функция FeedByDate() читала каждую ленту, которую ей передают, затем посылала ее в ReadFeed() вместе с датой, определенной на компоненте Calendar. Это довольно просто:

 bool FeedByDate(TreeModel model, TreePath path, TreeIter iter)
 {
 string url = (string)model.GetValue(iter, 0);
 ReadFeed(url, new DateTime(calendar.Year, calendar.Month + 1,
 calendar.Day));
 return false;
 }

Первая строка выцарапывает URL ленты из Tree View, после чего он передается методу ReadFeed() вместе с годом, месяцем и днем, выбранными в календаре. Но вы заметили calendar.Month + 1? GTK ведет отсчет месяцев с нуля. Зато дни и года он считает с 1. Я тоже не знаю, почему!

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

  • Вывод будет происходить в текстовый буфер, а не в консоль.
  • GUID’ы кэшироваться не будут – мы хотим, чтобы Chomp загружала все записи каждой ленты, а не записи, которые мы не видели.
  • Если для времени указано DateTime.MinValue, имеется в виду «по дате фильтровать не надо».
  • Иначе, получить дату публикации каждой новости и преобразовать ее в объект DateTime.
  • Затем сравнить каждый объект DateTime с указанной датой и вывести только те записи, которые ей соответствуют.

Тут есть мелкий просчет: одни ленты, включая Linux Format, предоставляют дату публикации в формате Wed, 24 Jan 2007 11:02:43 +0000, а другие вместо ‘+0000’ ставят ‘GMT’. .NET понимает оба формата, но в Mono – возможно, по недосмотру – поддерживается только последний. В качестве обходного пути мы будем обрезать всю подстроку, начиная с символа +.

[править] Время писать код

Вот и код. Если вы читали LXF89, то должны узнать его части!

  protected virtual void ReadFeed(string feed, DateTime filter) {
    XmlDocument doc = new XmlDocument();
    doc.Load(feed);
    TextBuffer text = txtFeed.Buffer;
    XmlNodeList items = doc.SelectNodes(//item”);
    foreach (XmlNode item in items) {
      if (filter == DateTime.MinValue) {
        text.Text += (item.SelectSingleNode(“title”).InnerText) + “\n”;
        text.Text += (“ “ + item.SelectSingleNode(“description”).
 InnerText) + “\n\n”;
      } else {
        string time = item.SelectSingleNode(“pubDate”).InnerText;
        if (time.Contains(+)) time = time.Substring(0, time.
  IndexOf(+));
        DateTime thisdate = DateTime.Parse(time);
        if (filter.Day == thisdate.Day && filter.Month == thisdate.Month
  && filter.Year == thisdate.Year) {
           text.Text += (item.SelectSingleNode(“title”).InnerText) + “\n”;
          text.Text += (“ “ + item.SelectSingleNode(“description”).
 InnerText) + “\n\n”;
        }
      }
    }
  }

Две строки могут вызвать затруднение:

  if (time.Contains(+)) time = time.Substring(0, time.IndexOf(+));
  DateTime thisdate = DateTime.Parse(time);

Первая означает «если строка содержит +, просьба записать в переменную time все от начала строки (от символа с номером ноль) до знака +»: таким образом отсекается часть +0000. Вторая строка означает «возьми строку time и преобразуй ее в тип DateTime'.» Хранить дату как DateTime удобнее, потому что можно работать с днем, месяцем и годом как с числами, а не разбирать строку вручную.

[править] Финишная прямая

Первая версия Chomp почти завершена, но в ней кое-чего не хватает. Двойной щелчок по лентам загружает все ленты. Двойной щелчок по дате загружает все ленты и фильтрует записи по выбранной дате. Но в какой момент мы загружаем ленты в программу? Ответ: а ни в какой!

Наш предыдущий XML-считыватель тоже загружал список лент, поэтому можно просто взять и модифицировать его для нашего интерфейса на GTK. А именно, давайте поместим каждый URL в Tree View, чтобы пользователи могли нажимать на них. Здесь опять проявится занудство GTK, поэтому возьмите себя в руки:

  string[] sitelist;
  if (File.Exists(“sitelist.txt)) {
    sitelist = File.ReadAllLines(“sitelist.txt);
  } else {
    sitelist = new string[0];
  }
  // это значит, что мы хотим сохранить строку в строке Tree View, но в
 действительности нам нужен только URL!
 TreeStore store = new TreeStore (typeof(string), typeof(string));
 foreach(string site in sitelist) {
   // “Foo” это место размещения
   store.AppendValues(site, “Foo”);
 }
 tvFeeds.Model = store;
 tvFeeds.AppendColumn(“URLs”, new CellRendererText(), “text”,0);

Чтобы показать данные, Tree View нужен по крайней мере один столбец. Смело отключайте заголовки в свойствах компонента Stetic, в них нет нужды. Теперь создайте файл sitelist.txt в вашем рабочем каталоге программы (наподобие /путь/до/проекта/bin/Debug), заполните его URL-адресами лент, и пусть поработает!

Весь код этого учебника, даже с некоторыми дополнительными возможностями (см. врезку Chomp 0.2), включен на диск. Но чтобы проект созрел для помещения на SourceForge, не мешает его усовершенствовать. Оставляю Chomp в ваших умелых руках... LXF

[править] Chomp 0.2

Версия Chomp на диске базируется на том, что мы сделали, и позволяет пользователям временно добавлять новые ленты, но вы можете сделать еще кое-что:

  • Заставить работать функцию удаления ленты.
  • Кэшировать ленты и GUID, чтобы при отображении они загружались из памяти, а не скачивались каждый раз из Сети.
  • Реализовать поиск в кэшированных лентах.
  • Добавить Refresh и RefreshAll.
  • Поместить что-нибудь в строку состояния!
  • Заполнить меню осмысленными действиями.
  • Добавлять пользовательские ленты в файл sitelist.txt.

Целью этого урока было научить вас использовать Stetic, и я думаю, мы справились неплохо. Дальнейшая работа будет в основном писа ниной кода: обвешаем интерфейс всякими крутыми штуками. Почаще сохраняйте результат, и все будет в порядке – резвитесь!

Персональные инструменты
купить
подписаться
Яндекс.Метрика