Delphi programming blog
Источник: http://teran.karelia.pro/articles/item_4438.html
 

XML & Context menu

Опубликовано 05.09.2010 г. 23:38

Предпоследний мой пост был про работу с API сайта myshows.ru, и хотел я написать про генерацию контекстных меню для сериалов. Меню должно было строиться индивидуально для каждого сериала, согласно конфигу, хранящемуся в xml формате.

Задача выглядела следующим образом: в таблице перечислены сериалы, которые смотрит пользователь, при вызове контекстного меню хотелось бы видеть:
  1. избранные ссылки (отображаются для всех сериалов)
  2. Ссылки для выбранного сериала
  3. Ссылки на видео файлы для новых серий
В избранные ссылки, например, можно добавить URL каких либо тематических сайтов. В "индивидуальные" ссылки для сериала можно добавить, к примеру, URL соответствующих тем на форумах, а также пути к папкам сериалов, локальных или на серверах. Что касается ссылок на новые серии, то тут мы можем оттолкнуться от следующего: формат названия серии для каждого сериала обычно постоянен вида "bones.s01e01.dvdrip.rus.eng.novafilm.tv.avi" следовательно сохранив шаблон для каждого сериала, мы можем искать подобные файлы в директориях, которые мы указали для сериала. Ниже иллюстрация вышеизложенного (:
 
Как я уже говорил, конфигурационный файл представлен в виде XML документа. Для работы с ним реализован класс TSeriesConfig описание класса следующее:
  TSeriesConfig = class(TObject)
    strict private
      xml : IXMLDocument;
      xmlFile : string;
      root : IXMLNode;
      function getNodeBySeriesID(showId : integer):IXMLNode;
    public
      constructor Create();
      procedure SaveConfig();
      procedure CommitSeriesLinksChanges(showId : integer; linksList : TStringList; pattern:string);
      function getLinksForShow(showId:integer):TStringList;
      function getEpisodePattern(showId:integer):string;
      function getFavoritesLinks():TStringList;
      function SearchForNewEpisodes(showId:integer; count : integer = 1; eId : integer = -1) : TStringList;
  end;
Работа с xml документом реализована с помощью IXMLDocument. Есть соответствующий класс TXMLDocument, но его нельзя создавать динамически, т.е во время выполнения программы. Разбираться в причинах сего я не стал, и поверил документации (: Правда проверил - не работает (: Данный класс весьма прост в использовании, экземпляр можно получить с помощью вызова функции newXMLDocument. Далее для работы потребуется не так уж и много методов. Загрзука данных из файла, как в принципе и везде с помощью LoadFromFile; корневой узел можно получить используя GetDocumentElement(), и если он отсутствует то создать его с помощью CreateElement(). Рассмотрим использование данных методов, на примере конструктора класса TSeriesConfig:
constructor TSeriesConfig.Create();
begin
    inherited;
    xml := newXMLDocument();
    xml.Encoding := 'Windows-1251';

    xmlFile := ExtractFilePath(application.ExeName) + XML_FILE_NAME;
    if FileExists(xmlFile) then begin
        xml.LoadFromFile(xmlFile);
    end;

    xml.Active := true;
    root := xml.DocumentElement;
    if root = nil then root := xml.CreateElement('SeriesConfig','');
end;
Сохранение документа обратно в файл осуществляется с помощью SaveToFile. Навигация по документу осуществляется с помощью методов узлов, и начинается с корневого узла. Узел - объект типа IXMLNode. У каждого узла есть коллекция дочерних узлов ChildNodes, перебирая которые мы можем найти нужный, либо воспользоваться методом FindNode(), добавлять дочерние узлы с помощью AddChild или удалять используя childNodes.Delete(index:integer). Атрибуты узла можно получить с помощью node.attributes[], содержание узла с помощью свойства text или xml. Метод childNodes.clear() удаляет все дочерние узлы. Таковы базовые методы класса, который в принципе достаточно для работы с xml документами.
function TSeriesConfig.getLinksForShow(showId:integer): TStringList;
var node : IXMLNode;
    i:integer;
    title, link : string;
begin
    result := TStringList.Create();

    node := self.getNodeBySeriesID(showId);
    if node = nil then exit;

    node := node.ChildNodes.FindNode(SERIES_LINKS);
    if node = nil then exit;

    for i:=0 to node.ChildNodes.Count - 1 do begin
        title := node.ChildNodes[i].Attributes['title'];
        link := node.ChildNodes[i].Attributes['link'];
        result.Values[title] := link;
    end;
end;
Приведенная функция getLinksForShow() возвращает список ссылок для выбранного сериала (showId) в TStringList в виде title=link. XML файл содержит подобные описания сериалов:
<series showId="115">
  <SeriesLinks>
    <LinkItem title="\\video\кости" link="\\10.0.11.1\video\Кости (Bones)\"/>
    <LinkItem title="serials" link="http://serial.karelia.ru/?mode=comments&id=7180#s=k"/>
  </SeriesLinks>
  <EpisodePattern value="bones.s%S%e%E%.dvdrip.rus.eng.novafilm.tv.avi"/>
</series>
как видно приведены 2 ссылки, одна из которых http, вторая - директория, а также описан шаблон для имен файлов серий. Теперь получив ссылки и шаблон, мы хотим на их основе сформировать контекстное меню. С помощью данного шаблона потребуется провести поиск в ссылках-директориях, используя методы TDirectory для просмотра поддиректорий. в подстановочные знаки %s% & %e% будем подставлять требуемые номера сезона, и номера эпизода. А искать будем ближайшие три не просмотренные серии. Предположим, что мы имеем список директорий links, в котором требуется провести поиск count файлов ближайших серий. Имя искомого файла будем получать с помощью метода getNextEpisodeFileName(). Далее проведем поиск в указанной директории, и если потребуется также поддиректориях. Список поддиректорий получим с помощью TDirectory.getDirectories()
    for k:=1 to count do begin
        fileName := getNextEpisodeFileName();
        found := false;
        for i:=0 to links.Count - 1 do begin
            path := links.ValueFromIndex[i] ;
            if FileExists(path + fileName) then begin
                result.Add(path + fileName);
                break;
            end;

            for subDir in TDirectory.GetDirectories(path) do  begin
                if FileExists(subDir +  '\' +FileName) then begin
                    result.Add(subDir +'\' +  FileName);
                    found := true;
                    break;
                end;
            end;
            if found then break;
        end;
    end;
Ок, теперь мы имеем все что требуется для конструирования меню. Рассмотрим вызов контекстного меню таблицы (TMS Grid). Параметр handled указывает обработано ли меню. Если мы установим его в true то меню показано не будет, что мы и сделаем, если нас не устраивает строка на которой был клик (например, фиксированная строка заголовка). Контекстное меню имеет имя SeriesPopupMenu, и фиксированный элемент "Ссылки", который имеет свойство tag = 0, его мы никогда не будем удалять, остальные же элементы, которые мы создаем будут иметь tag отличный от нуля. Далее получим список наших ссылок, избранных и обычных, а также список новых серий. После чего воспользуемся вложенной процедурой AddMenuItem для добавления пунктов меню по списку.
procedure TMainForm.seriesGridContextPopup(Sender: TObject; MousePos: TPoint;  var Handled: Boolean);
var col, row : integer;
    showId : integer;
    links : TStringList;
    i:integer;
    fileName : string;

        procedure addMenuItem(key, action:string); 

begin
    seriesGrid.MouseToCell(mousePos.X, mousePos.Y, col, row);
    if row > 0 then begin
        handled := false;
        showId := seriesGrid.ints[2,row];

        i := SeriesPopupMenu.Items.Count - 1 ;
        while SeriesPopupMenu.Items[i].Tag >0  do begin
            SeriesPopupMenu.Items[i].Free();
            dec(i);
        end;

        links := TStringList.Create();
        links.Assign(linksConfig.getFavoritesLinks());
        links.Append('-=-');
        links.AddStrings(linksConfig.getLinksForShow(showId));
        links.Append('-=-');
        for fileName in linksConfig.SearchForNewEpisodes(ShowId, 3) do begin
            links.Values[extractFileName(fileName)] := fileName;
        end;

        for i:=0 to links.Count - 1 do begin
            addMenuItem(links.Names[i], links.ValueFromIndex[i]);
        end;

        links.Free();
    end
    else handled := true;
end;
Тут возникает вопрос, мы можем добавить в пункт меню его заголовок caption, присвоить ему обработчик в OnClick. Но для передачи строки (директории, URL или имени файла серии) очевидных путей нет. Поэтому мы воспользуемся весьма известным фокусом, записав в свойство tag адрес буфера, в котором содержится требуемая нам строка, для чего нам потребуются функции getMem & copyMemory.
        procedure addMenuItem(key, action:string);
        var len : integer;
            menuItem : TMenuItem;
            linkAction : PChar;
        begin
            menuItem := TMenuItem.Create(seriesPopupMenu);
            menuItem.Caption := key;
            menuItem.Tag := 1;              //to delete

            if length(action) >= 3 then begin
                len := (length(action) + 1) * 2;
                getMem(linkAction, len);
                CopyMemory(linkAction, PChar(action), len);

                menuItem.Tag := Integer(linkAction);
                menuItem.OnClick := SeriesPopupMenuLinkClick;

                if StartsText('http', action) then menuItem.ImageIndex := 6;
                if StartsText('\\',   action) then menuItem.ImageIndex := 5;
                if EndsText(key, action) then menuItem.ImageIndex := 7;
            end;
            SeriesPopupMenu.Items.Add(menuItem);
        end;
В обработчике нажатия пункта меню нам потребуется обратно получить нашу строку из поля tag. вызвать требуемое действие, и очистить выделенную для буфера память.
procedure TMainForm.SeriesPopupMenuLinkClick(Sender: TObject);
var action : PWideChar;
begin
    action := PChar(TComponent(sender).Tag);

    ShellExecute(handle, 'open', action, nil, nil, SW_SHOWNORMAL);

    freeMem(action);
end;
Есть одно плохое свойство при подобном конструировании меню: при поиске файлов серий возникает небольшая задержка, которая в принципе может быть и достаточно большой. Есть мысль проводить поиск в отдельном потоке, и по мере нахождения файлов добавлять их в уже открытое меню, только пока что неизвестно как, но наверное это возможно. В динамическом создании пунктов меню есть один нюанс. Год назад в одном из проектов в главном меню программы надо было динамический строить список пунктов. Меню было от TMS, все было красиво, а вот новые пункты получались стандартные виндовые, что сначала меня поставило в тупик, и в последующем решилось присваиванием новым пунктам обработчика OnAdvancedDraw от родительского пункта, что впрочем логично. Однако используя контекстные меню, которые будут строиться полностью динамически, заимствовать метод рисования неоткуда. кстати говоря о TMS, почему то всегда когда начинаешь делать что либо с использованием их компонентов (Grid/InspectorBar) каждый раз натыкаешься на какие то баги. Grid уже настолько большой что в нем без мануала не разобраться, а начиная настраивать одно, почему то перестает работать другое. Вот и сейчас, заменив стандартные гриды в приложении на TMS после изменения какого то из свойств, перестала корректно работать подсветка строки при наведении мыши (hovering) и еще какой то глюк был. Наилучшим вариантом было бы использование devExpress грида, но для него прийдется написать customDataSet, для списка сериалов и эпизодов.
Метки:  IXMLDoc  |  myshows  |  xml 

Комментарии

BrendanDeemn
29.04.2017 в 14:18
Нашел привлекательный сайт с возможностью просмотра фильмов онлайн и очень много фильмов на сайте, делюсь им с вами, не благодарите)
http://kinozor.net
А так же на сайте есть раздел новинок за 2017 год
http://kinozor.net/filmy-2017/
А еще мне понравились эти фильмы
http://kinozor.net/filmy-2017/15813-prizrak-v-dospehah.html
http://kinozor.net/filmy-2017/15709-robo-dog-airborne.html
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно