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

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

Здравствуйте. Я думаю, у каждого программиста есть свой набор примочек, программ, которые делают процесс работы проще и приятней. Сегодня речь пойдет об одной из таких программ, а точнее о том, как использовать списки переходов (jump lists), дабы уменьшить количество ярлыков на рабочем столе. Вот, что мы получим в итоге:
За подробностями прошу под кат.


Итак, начальные условия:
1. У меня есть около 15 программ, которыми я пользуюсь часто (хотя бы раз в день). Хочу иметь возможность запускать эти программы быстро и удобно
2. Большая часть этих программ лежит в папке, которая синхронизируется между всеми моими компами/виртуалками
3. Общее между всеми компами то, что там везде установлена Windows 7

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

Как я это вижу:

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

Инструменты:
1. Списки переходов
2. Я выбрал WPF, так как это моя слабая сторона. Повышаю опыт.
3. MVVM Light Toolkit

Ну, вроде всё. Поехали!

Для начала я набросал класс этого описания программы

  1. namespace TaskStart.Tasks
  2. {
  3.     [Serializable]
  4.     public class Task
  5.     {
  6.         [XmlAttribute]
  7.         public string ApplicationPath { get; set; }
  8.  
  9.         [XmlAttribute]
  10.         public string Title { get; set; }
  11.  
  12.         [XmlAttribute]
  13.         public string Description { get; set; }
  14.  
  15.         [XmlAttribute]
  16.         public string Category { get; set; }
  17.     }
  18. }

Тут ничего сложного. Далее, мне надо было определить механизм сохранения/загрузки. Я сделал простой класс Settings. Синглтоном я его сделал ради забавы, мог бы реализовать и по другому, но мне так больше нравится. Комментариев я не писал, так как код ну очень простой.

  1. public class Settings
  2. {
  3.     private const string SettingsFileName = "Settings.xml";
  4.  
  5.     private Settings()
  6.     {
  7.  
  8.     }
  9.  
  10.     private static Settings _instance;
  11.     public static Settings Instance
  12.     {
  13.         get { return _instance ?? (_instance = new Settings()); }
  14.     }
  15.  
  16.     public IEnumerable<Task> GetTasks()
  17.     {
  18.         var result = new List<Task>();
  19.         if (File.Exists(SettingsFileName))
  20.         {
  21.             try
  22.             {
  23.                 var xmlSer = new XmlSerializer(typeof(List<Task>));
  24.                 using (var sr = new StreamReader(SettingsFileName))
  25.                 {
  26.                     var res = xmlSer.Deserialize(sr) as List<Task>;
  27.                     result = res ?? new List<Task>();
  28.                 }
  29.             }
  30.             catch (Exception)
  31.             {
  32.                 return result;
  33.             }
  34.         }
  35.         return result;
  36.     }
  37.  
  38.     public void SetTasks(IEnumerable<Task> tasks)
  39.     {
  40.         var taskList = tasks.ToList();
  41.         using (var sw = new StreamWriter(SettingsFileName, false))
  42.         {
  43.             var xmlSer = new XmlSerializer(typeof(List<Task>));
  44.             xmlSer.Serialize(sw, taskList);
  45.         }
  46.     }
  47. }

Вот пример полученного на выходе XML

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <ArrayOfTask xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  3.   <Task ApplicationPath="D:\Pub\Tools\foobar2000\foobar2000.exe" Title="foobar2000" Category="Entertainment" />
  4.   <Task ApplicationPath="D:\Pub\Tools\The KMPlayer\KMPlayer.exe" Title="KMPlayer" Category="Entertainment" />
  5.   <Task ApplicationPath="D:\Pub\Miranda IM\MirandaPortable.exe" Title="Miranda" Category="Messaging" />
  6.   <Task ApplicationPath="C:\Program Files (x86)\Skype\Phone\Skype.exe" Title="Skype" Category="Messaging" />
  7.   <Task ApplicationPath="D:\Pub\Tools\DotNetNotepad\DotNetNotepad.UI.exe" Title="DotNetNotepad" Category="Office" />
  8.   <Task ApplicationPath="C:\Program Files (x86)\Evernote\Evernote\Evernote.exe" Title="Evernote" Category="Office" />
  9.   <Task ApplicationPath="C:\Users\AMuradov\AppData\Local\Apps\Evernote\Evernote\Evernote.exe" Title="Evernote" Category="Office" />
  10.   <Task ApplicationPath="D:\Pub\Tools\Notepad++\notepad++.exe" Title="Notepad++" Category="Office" />
  11.   <Task ApplicationPath="D:\Projects\TTK\AMuradov\AdHelper\AdHelper.App\bin\Release\AdHelper.App.exe" Title="AdHelper.App" Category="Programming" />
  12.   <Task ApplicationPath="D:\Pub\Tools\Far\x64\FarEmu\ConEmu64.exe" Title="Far x64" Category="Programming" />
  13.   <Task ApplicationPath="D:\Pub\Tools\SQL Tools\SQL.Manager\MsManager.exe" Title="MsManager" Category="Programming" />
  14.   <Task ApplicationPath="D:\Pub\Tools\Rad Software Regular Expression Designer\Rad.RegexDesigner.exe" Title="Rad.RegexDesigner" Category="Programming" />
  15.   <Task ApplicationPath="C:\Program Files (x86)\Microsoft SQL Server\100\Tools\Binn\VSShell\Common7\IDE\Ssms.exe" Title="Sql Management Studio" Category="Programming" />
  16.   <Task ApplicationPath="C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\devenv.exe" Title="Visual Studio 2010" Category="Programming" />
  17.   <Task ApplicationPath="C:\Users\\uc1Артем\AppData\Local\Google\Chrome\Application\chrome.exe" Title="Chrome" Category="Web" />
  18.   <Task ApplicationPath="C:\Users\AMuradov\AppData\Local\Google\Chrome\Application\chrome.exe" Title="Chrome" Category="Web" />
  19.   <Task ApplicationPath="C:\Program Files (x86)\Mozilla Firefox\firefox.exe" Title="Fierfox" Description="Web" Category="Web" />
  20.   <Task ApplicationPath="C:\Program Files (x86)\Internet Explorer\iexplore.exe" Title="IE" Category="Web" />
  21. </ArrayOfTask>

Далее, мне для моей сущности Task нужна была ViewModel - именно её я буду привязывать к интерфейсу.

  1. public class TaskViewModel
  2. {
  3.     private readonly Task _task;
  4.  
  5.     /// <summary>
  6.     /// Конструктор. Принимает Task и заполняет нужные поля данными. Также инициализирует команду запуска приложения.
  7.     /// </summary>
  8.     /// <param name="task"></param>
  9.     public TaskViewModel( Task task)
  10.     {
  11.         _task = task;
  12.         
  13.         ApplicationPath = _task.ApplicationPath;
  14.         Category = _task.Category;
  15.         Description = _task.Description;
  16.         Title = _task.Title;
  17.  
  18.         // Запуск приложения
  19.         StartCommand = new RelayCommand(()=>
  20.                                             {
  21.                                                 if (File.Exists(ApplicationPath))
  22.                                                     Process.Start(ApplicationPath);
  23.                                             },
  24.                                             ()=> File.Exists(ApplicationPath));
  25.     }
  26.  
  27.     /// <summary>
  28.     /// Применяется для получения экземпляра Task - понадобится при сохранении информации из интерфейса в файл
  29.     /// </summary>
  30.     /// <returns></returns>
  31.     public Task GetTask()
  32.     {
  33.         return new Task
  34.                    {
  35.                     // Это поле нельзя изменять через интерфейс. Для этого я и оставил переменную  _task
  36.                     ApplicationPath = _task.ApplicationPath,
  37.                     Category = Category,
  38.                     Description = Description,
  39.                     Title = Title
  40.                    };
  41.     }
  42.  
  43.     
  44.     /// <summary>
  45.     /// Публичные поля
  46.     /// </summary>
  47.     public string ApplicationPath { get; set; }
  48.     public string Title { get; set; }
  49.     public string Description { get; set; }
  50.     public string Category { get; set; }
  51.     
  52.     // Команды
  53.     public ICommand StartCommand { get; set; }
  54.     
  55.     /// <summary>
  56.     /// Получение иконки. Используется для показа иконки приложения.
  57.     /// Как ни старался, так и не решил проблему с цветом. То есть у результирующей иконки палитра цветов отличается от оригинальной.
  58.     /// </summary>
  59.     private object _icon;
  60.     public object Icon
  61.     {
  62.         get
  63.         {
  64.             if (_icon == null)
  65.             {
  66.                 try
  67.                 {
  68.                     var ico = System.Drawing.Icon.ExtractAssociatedIcon(ApplicationPath);
  69.  
  70.                     if (ico != null)
  71.                     {
  72.                         using (var strm = new MemoryStream())
  73.                         {
  74.                             ico.Save(strm);
  75.                             var ibd = new IconBitmapDecoder(strm, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
  76.                             var frame = ibd.Frames.FirstOrDefault();
  77.                             _icon = frame;
  78.                         }
  79.                     }
  80.                 }
  81.                 catch
  82.                 {
  83.                     _icon = null;
  84.                 }
  85.             }
  86.             return _icon;
  87.         }
  88.     }
  89. }

Теперь самая главная наша ViewModel.

  1. public class MainViewModel : ViewModelBase
  2. {
  3.     /// <summary>
  4.     /// Список переходов
  5.     /// </summary>
  6.     readonly JumpList _jumpList;
  7.  
  8.     public MainViewModel()
  9.     {
  10.         // Инициализируем список и коллекцию
  11.         _jumpList = new JumpList();
  12.         JumpList.SetJumpList(Application.Current, _jumpList);
  13.  
  14.         _tasks = new ObservableCollection<TaskViewModel>();
  15.         
  16.         // Загружаем из файла задачи
  17.         Load();
  18.  
  19.         // Обновляем список переходов
  20.         Apply();
  21.  
  22.         // Команда обновления списка переходов
  23.         ApplyCommand = new RelayCommand(Apply);
  24.     }
  25.     
  26.     /// <summary>
  27.     /// Команды
  28.     /// </summary>
  29.     public ICommand ApplyCommand { get; set; }
  30.  
  31.     /// <summary>
  32.     /// Коллекция задач
  33.     /// </summary>
  34.     private ObservableCollection<TaskViewModel> _tasks;
  35.     public ObservableCollection<TaskViewModel> Tasks
  36.     {
  37.         get { return _tasks; }
  38.         set
  39.         {
  40.             if (_tasks != value)
  41.             {
  42.                 _tasks = value;
  43.                 RaisePropertyChanged("Tasks");
  44.             }
  45.         }
  46.     }
  47.  
  48.  
  49.     /// <summary>
  50.     /// Функция, перезаписывает список переходов.
  51.     /// </summary>
  52.     public void Apply()
  53.     {
  54.         // очистка существующего списка
  55.         _jumpList.JumpItems.Clear();
  56.  
  57.         // получаем JumpTask
  58.         var jumpTasks =
  59.             (from task in Tasks.Select(x=>x.GetTask())
  60.              where File.Exists(task.ApplicationPath)
  61.              orderby task.Title, task.Category
  62.              select new JumpTask
  63.                         {
  64.                             Title = task.Title ?? string.Empty,
  65.                             Description = task.Description ?? string.Empty,
  66.                             ApplicationPath = task.ApplicationPath ?? string.Empty,
  67.                             IconResourcePath = task.ApplicationPath ?? string.Empty,
  68.                             WorkingDirectory = Path.GetDirectoryName(task.ApplicationPath),
  69.                             CustomCategory = task.Category ?? string.Empty,
  70.                         }).ToList();
  71.         // Шаманим с сортировкой. Этот код вообще не обязателен, просто мне нужен был список в определенном порядке
  72.         jumpTasks.Reverse();
  73.         // добавляем все JumpTask в список jumpTasks
  74.         jumpTasks.ForEach(_jumpList.JumpItems.Add);
  75.         // применяем изменения
  76.         _jumpList.Apply();
  77.         // сохраняем список в файл
  78.         Save();
  79.     }
  80.  
  81.     // Сохранение списка в файл
  82.     public void Save()
  83.     {
  84.         Settings.Instance.SetTasks(Tasks.Select(x=>x.GetTask()));
  85.     }
  86.  
  87.     // Загрузка списка из файла
  88.     public void Load()
  89.     {
  90.         try
  91.         {
  92.             var tasks = Settings.Instance.GetTasks();
  93.             foreach (var task in tasks.OrderBy(x=>x.Category).ThenBy(x=>x.Title))
  94.             {
  95.                 _tasks.Add(new TaskViewModel(task));
  96.             }
  97.         }
  98.         catch
  99.         {
  100.             _tasks = new ObservableCollection<TaskViewModel>();
  101.         }
  102.     }
  103.  
  104.     // Добавление нового элемента в список.
  105.     public void Add(string fileName)
  106.     {
  107.         var task = new Task { ApplicationPath = fileName, Category = string.Empty, Title = Path.GetFileNameWithoutExtension(fileName) };
  108.         _tasks.Add(new TaskViewModel(task));
  109.     }
  110. }

Осталось только прибиндить это всё к гриду. Вот как я это сделал:

  1. <DataGrid x:Name="dg"
  2.     Grid.Row="2" ItemsSource="{Binding Tasks, Mode=TwoWay}" AutoGenerateColumns="False" CanUserReorderColumns="True" CanUserResizeColumns="True"
  3.   CanUserResizeRows="False" CanUserSortColumns="True" CanUserAddRows="True" CanUserDeleteRows="True" Margin="5" >
  4.     <DataGrid.Columns>
  5.         <DataGridTemplateColumn>
  6.             <DataGridTemplateColumn.CellTemplate>
  7.                 <DataTemplate>
  8.                     <Image Source="{Binding Icon, Mode=OneWay}" Stretch="Uniform" Width="16"></Image>
  9.                 </DataTemplate>
  10.             </DataGridTemplateColumn.CellTemplate>
  11.         </DataGridTemplateColumn>
  12.         <DataGridTextColumn Header="Category" Binding="{Binding Category, Mode=TwoWay}" Width="Auto"></DataGridTextColumn>
  13.         <DataGridTextColumn Header="Title" Binding="{Binding Title, Mode=TwoWay}" Width="Auto"></DataGridTextColumn>
  14.         <DataGridTextColumn Header="Description" Binding="{Binding Description, Mode=TwoWay}" Width="Auto"></DataGridTextColumn>
  15.         <DataGridTemplateColumn Header="ApplicationPath" Width="*">
  16.             <DataGridTemplateColumn.CellTemplate>
  17.                 <DataTemplate>
  18.                     <Button VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="2" CommandParameter="{Binding ApplicationPath}" Command="{Binding StartCommand}" >
  19.                         <Button.Background>
  20.                             <SolidColorBrush Color="Transparent"></SolidColorBrush>
  21.                         </Button.Background>
  22.                         <Button.Content>
  23.                             <TextBlock Text="{Binding ApplicationPath, Mode=OneWay}"></TextBlock>
  24.                         </Button.Content>
  25.                     </Button>
  26.                 </DataTemplate>
  27.             </DataGridTemplateColumn.CellTemplate>
  28.         </DataGridTemplateColumn>                
  29.     </DataGrid.Columns>
  30. </DataGrid>

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

  1. <ToolBar Grid.Row="1">    
  2.     <Button Command="{Binding ApplyCommand}">
  3.         <Image Source="Images/apply.png" Height="16"/>
  4.     </Button>            
  5. </ToolBar>

Итак, теперь я могу редактировать мои задачи прямо в интерфейсе. Но я ещё не могу добавить их туда. Я решил использовать событие Drop главного окна, чтобы легко добавлять новые записи в мой список задач.

  1. private void WindowDrop(object sender, DragEventArgs e)
  2. {
  3.     if (e.Data is DataObject && ((DataObject)e.Data).ContainsFileDropList())
  4.     {
  5.         foreach (string filePath in ((DataObject)e.Data).GetFileDropList())
  6.         {
  7.             if (File.Exists(filePath))
  8.                 _model.Add(filePath);
  9.         }
  10.     }
  11. }

В итоге, я получил приложение, которое позволяет синхронизировать мои списки переходов между всеми компьютерами. Также я предусмотрел, что если приложения, которое я указал на первом ПК, на втором ПК нет или путь к нему другой, - то ссылка на него на втором ПК не появится. Вот скрины моих результатов:


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

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

На этом всё. Всем спасибо.

UPD. Думаю, следует пояснить. В классе TaskViewModel команда StartCommand запускает приложение. Это я сделал только для того, чтобы у меня была возможность запустить приложение из моей программы. К списку переходов эта команда не имеет никакого отношения. Вся работа со списком переходов в классе MainViewModel

  1. /// <summary>
  2. /// Функция, перезаписывает список переходов.
  3. /// </summary>
  4. public void Apply()
  5. {
  6.     // очистка существующего списка
  7.     _jumpList.JumpItems.Clear();
  8.  
  9.     // получаем JumpTask
  10.     var jumpTasks =
  11.         (from task in Tasks.Select(x=>x.GetTask())
  12.          where File.Exists(task.ApplicationPath)
  13.          orderby task.Title, task.Category
  14.          select new JumpTask
  15.                     {
  16.                         Title = task.Title ?? string.Empty,
  17.                         Description = task.Description ?? string.Empty,
  18.                         ApplicationPath = task.ApplicationPath ?? string.Empty,
  19.                         IconResourcePath = task.ApplicationPath ?? string.Empty,
  20.                         WorkingDirectory = Path.GetDirectoryName(task.ApplicationPath),
  21.                         CustomCategory = task.Category ?? string.Empty,
  22.                     }).ToList();
  23.     // Шаманим с сортировкой. Этот код вообще не обязателен, просто мне нужен был список в определенном порядке
  24.     jumpTasks.Reverse();
  25.     // добавляем все JumpTask в список jumpTasks
  26.     jumpTasks.ForEach(_jumpList.JumpItems.Add);
  27.     // применяем изменения
  28.     _jumpList.Apply();
  29.     // сохраняем список в файл
  30.     Save();
  31. }

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


UPD2. В комментариях попросили выложить исходники. Вот они.

7 комментариев:

  1. Очень интересно!
    Есть возможность запустить какое-нибудь приложение от администратора?

    ОтветитьУдалить
  2. А, ясно. То есть это ограничение Windows для Jump списка.

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

    ОтветитьУдалить
  4. Синхронизировать можно разными программами (Дропбокс, Windows Live Mesh). А за иконки спасибо, обязательно почитаю и попробую.

    ОтветитьУдалить
  5. А нельзя ли исходники выложить?
    Спасибо.

    ОтветитьУдалить