Здравствуйте. Я думаю, у каждого программиста есть свой набор примочек, программ, которые делают процесс работы проще и приятней. Сегодня речь пойдет об одной из таких программ, а точнее о том, как использовать списки переходов (jump lists), дабы уменьшить количество ярлыков на рабочем столе. Вот, что мы получим в итоге:
За подробностями прошу под кат.
Итак, начальные условия:
1. У меня есть около 15 программ, которыми я пользуюсь часто (хотя бы раз в день). Хочу иметь возможность запускать эти программы быстро и удобно
2. Большая часть этих программ лежит в папке, которая синхронизируется между всеми моими компами/виртуалками
3. Общее между всеми компами то, что там везде установлена Windows 7
Задача:
Написать программу, которая также будет синхронизирована между всеми компами и хранить информацию о запускаемых приложениях, давая возможность запускать эти приложения.
Как я это вижу:
Для каждой программы надо будет сделать описание (путь к exe файлу, название программы, описание и категория - это как минимум). Набор этих описаний я буду хранить в файле в той же папке что и саму программу. Тогда, благодаря синхронизации, у меня будет один общий список запускаемых программ для всех компьютеров. В самой программе нужно реализовать механизм заполнения списков переходов, запуска приложения из программы, ну и возможность редактирования названия и описания приложения.
Инструменты:
1. Списки переходов
2. Я выбрал WPF, так как это моя слабая сторона. Повышаю опыт.
3. MVVM Light Toolkit
Ну, вроде всё. Поехали!
Для начала я набросал класс этого описания программы
Тут ничего сложного. Далее, мне надо было определить механизм сохранения/загрузки. Я сделал простой класс Settings. Синглтоном я его сделал ради забавы, мог бы реализовать и по другому, но мне так больше нравится. Комментариев я не писал, так как код ну очень простой.
Вот пример полученного на выходе XML
Далее, мне для моей сущности Task нужна была ViewModel - именно её я буду привязывать к интерфейсу.
Теперь самая главная наша ViewModel.
Осталось только прибиндить это всё к гриду. Вот как я это сделал:
Благодаря двухсторонней привязке, у меня есть возможность редактировать мой список задач. Ещё я добавил панель инструментов с одной кнопкой - для сохранения и обновления списка переходов.
Итак, теперь я могу редактировать мои задачи прямо в интерфейсе. Но я ещё не могу добавить их туда. Я решил использовать событие Drop главного окна, чтобы легко добавлять новые записи в мой список задач.
В итоге, я получил приложение, которое позволяет синхронизировать мои списки переходов между всеми компьютерами. Также я предусмотрел, что если приложения, которое я указал на первом ПК, на втором ПК нет или путь к нему другой, - то ссылка на него на втором ПК не появится. Вот скрины моих результатов:
Теперь об ограничениях. Моя программа никак не работает с папками, она никак не работает с аргументами приложений, и вдобавок ей нужны именно *.exe файлы, ярлыки тоже никак не отработают. Меня все эти ограничения вполне устраивают, поэтому я ими не занимался.
Я было хотел выложить код программы на кодеплекс, но, как оказалось (и что, собственно, очевидно), использовать списки переходов для быстрого запуска программ идея не новая. Просто той функциональности, что была нужна мне, я в других программах не нашел, да и пощупать возможности панели задач уже давно чесались руки. Поэтому я свои исходники публиковать не буду (к тому же, я почти весь код в статье написал), а просто приведу несколько программ, которые решают подобные задачи.
Ещё ссылки:
На этом всё. Всем спасибо.
UPD. Думаю, следует пояснить. В классе TaskViewModel команда StartCommand запускает приложение. Это я сделал только для того, чтобы у меня была возможность запустить приложение из моей программы. К списку переходов эта команда не имеет никакого отношения. Вся работа со списком переходов в классе MainViewModel
Весь остальной код относится к работе непосредственно самой программы. Список переходов же доступен и тогда, когда программа не запущена.
За подробностями прошу под кат.
Итак, начальные условия:
1. У меня есть около 15 программ, которыми я пользуюсь часто (хотя бы раз в день). Хочу иметь возможность запускать эти программы быстро и удобно
2. Большая часть этих программ лежит в папке, которая синхронизируется между всеми моими компами/виртуалками
3. Общее между всеми компами то, что там везде установлена Windows 7
Задача:
Написать программу, которая также будет синхронизирована между всеми компами и хранить информацию о запускаемых приложениях, давая возможность запускать эти приложения.
Как я это вижу:
Для каждой программы надо будет сделать описание (путь к exe файлу, название программы, описание и категория - это как минимум). Набор этих описаний я буду хранить в файле в той же папке что и саму программу. Тогда, благодаря синхронизации, у меня будет один общий список запускаемых программ для всех компьютеров. В самой программе нужно реализовать механизм заполнения списков переходов, запуска приложения из программы, ну и возможность редактирования названия и описания приложения.
Инструменты:
1. Списки переходов
2. Я выбрал WPF, так как это моя слабая сторона. Повышаю опыт.
3. MVVM Light Toolkit
Ну, вроде всё. Поехали!
Для начала я набросал класс этого описания программы
- namespace TaskStart.Tasks
- {
- [Serializable]
- public class Task
- {
- [XmlAttribute]
- public string ApplicationPath { get; set; }
- [XmlAttribute]
- public string Title { get; set; }
- [XmlAttribute]
- public string Description { get; set; }
- [XmlAttribute]
- public string Category { get; set; }
- }
- }
Тут ничего сложного. Далее, мне надо было определить механизм сохранения/загрузки. Я сделал простой класс Settings. Синглтоном я его сделал ради забавы, мог бы реализовать и по другому, но мне так больше нравится. Комментариев я не писал, так как код ну очень простой.
- public class Settings
- {
- private const string SettingsFileName = "Settings.xml";
- private Settings()
- {
- }
- private static Settings _instance;
- public static Settings Instance
- {
- get { return _instance ?? (_instance = new Settings()); }
- }
- public IEnumerable<Task> GetTasks()
- {
- var result = new List<Task>();
- if (File.Exists(SettingsFileName))
- {
- try
- {
- var xmlSer = new XmlSerializer(typeof(List<Task>));
- using (var sr = new StreamReader(SettingsFileName))
- {
- var res = xmlSer.Deserialize(sr) as List<Task>;
- result = res ?? new List<Task>();
- }
- }
- catch (Exception)
- {
- return result;
- }
- }
- return result;
- }
- public void SetTasks(IEnumerable<Task> tasks)
- {
- var taskList = tasks.ToList();
- using (var sw = new StreamWriter(SettingsFileName, false))
- {
- var xmlSer = new XmlSerializer(typeof(List<Task>));
- xmlSer.Serialize(sw, taskList);
- }
- }
- }
Вот пример полученного на выходе XML
- <?xml version="1.0" encoding="utf-8"?>
- <ArrayOfTask xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Task ApplicationPath="D:\Pub\Tools\foobar2000\foobar2000.exe" Title="foobar2000" Category="Entertainment" />
- <Task ApplicationPath="D:\Pub\Tools\The KMPlayer\KMPlayer.exe" Title="KMPlayer" Category="Entertainment" />
- <Task ApplicationPath="D:\Pub\Miranda IM\MirandaPortable.exe" Title="Miranda" Category="Messaging" />
- <Task ApplicationPath="C:\Program Files (x86)\Skype\Phone\Skype.exe" Title="Skype" Category="Messaging" />
- <Task ApplicationPath="D:\Pub\Tools\DotNetNotepad\DotNetNotepad.UI.exe" Title="DotNetNotepad" Category="Office" />
- <Task ApplicationPath="C:\Program Files (x86)\Evernote\Evernote\Evernote.exe" Title="Evernote" Category="Office" />
- <Task ApplicationPath="C:\Users\AMuradov\AppData\Local\Apps\Evernote\Evernote\Evernote.exe" Title="Evernote" Category="Office" />
- <Task ApplicationPath="D:\Pub\Tools\Notepad++\notepad++.exe" Title="Notepad++" Category="Office" />
- <Task ApplicationPath="D:\Projects\TTK\AMuradov\AdHelper\AdHelper.App\bin\Release\AdHelper.App.exe" Title="AdHelper.App" Category="Programming" />
- <Task ApplicationPath="D:\Pub\Tools\Far\x64\FarEmu\ConEmu64.exe" Title="Far x64" Category="Programming" />
- <Task ApplicationPath="D:\Pub\Tools\SQL Tools\SQL.Manager\MsManager.exe" Title="MsManager" Category="Programming" />
- <Task ApplicationPath="D:\Pub\Tools\Rad Software Regular Expression Designer\Rad.RegexDesigner.exe" Title="Rad.RegexDesigner" Category="Programming" />
- <Task ApplicationPath="C:\Program Files (x86)\Microsoft SQL Server\100\Tools\Binn\VSShell\Common7\IDE\Ssms.exe" Title="Sql Management Studio" Category="Programming" />
- <Task ApplicationPath="C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\devenv.exe" Title="Visual Studio 2010" Category="Programming" />
- <Task ApplicationPath="C:\Users\\uc1Артем\AppData\Local\Google\Chrome\Application\chrome.exe" Title="Chrome" Category="Web" />
- <Task ApplicationPath="C:\Users\AMuradov\AppData\Local\Google\Chrome\Application\chrome.exe" Title="Chrome" Category="Web" />
- <Task ApplicationPath="C:\Program Files (x86)\Mozilla Firefox\firefox.exe" Title="Fierfox" Description="Web" Category="Web" />
- <Task ApplicationPath="C:\Program Files (x86)\Internet Explorer\iexplore.exe" Title="IE" Category="Web" />
- </ArrayOfTask>
Далее, мне для моей сущности Task нужна была ViewModel - именно её я буду привязывать к интерфейсу.
- public class TaskViewModel
- {
- private readonly Task _task;
- /// <summary>
- /// Конструктор. Принимает Task и заполняет нужные поля данными. Также инициализирует команду запуска приложения.
- /// </summary>
- /// <param name="task"></param>
- public TaskViewModel( Task task)
- {
- _task = task;
- ApplicationPath = _task.ApplicationPath;
- Category = _task.Category;
- Description = _task.Description;
- Title = _task.Title;
- // Запуск приложения
- StartCommand = new RelayCommand(()=>
- {
- if (File.Exists(ApplicationPath))
- Process.Start(ApplicationPath);
- },
- ()=> File.Exists(ApplicationPath));
- }
- /// <summary>
- /// Применяется для получения экземпляра Task - понадобится при сохранении информации из интерфейса в файл
- /// </summary>
- /// <returns></returns>
- public Task GetTask()
- {
- return new Task
- {
- // Это поле нельзя изменять через интерфейс. Для этого я и оставил переменную _task
- ApplicationPath = _task.ApplicationPath,
- Category = Category,
- Description = Description,
- Title = Title
- };
- }
- /// <summary>
- /// Публичные поля
- /// </summary>
- public string ApplicationPath { get; set; }
- public string Title { get; set; }
- public string Description { get; set; }
- public string Category { get; set; }
- // Команды
- public ICommand StartCommand { get; set; }
- /// <summary>
- /// Получение иконки. Используется для показа иконки приложения.
- /// Как ни старался, так и не решил проблему с цветом. То есть у результирующей иконки палитра цветов отличается от оригинальной.
- /// </summary>
- private object _icon;
- public object Icon
- {
- get
- {
- if (_icon == null)
- {
- try
- {
- var ico = System.Drawing.Icon.ExtractAssociatedIcon(ApplicationPath);
- if (ico != null)
- {
- using (var strm = new MemoryStream())
- {
- ico.Save(strm);
- var ibd = new IconBitmapDecoder(strm, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
- var frame = ibd.Frames.FirstOrDefault();
- _icon = frame;
- }
- }
- }
- catch
- {
- _icon = null;
- }
- }
- return _icon;
- }
- }
- }
Теперь самая главная наша ViewModel.
- public class MainViewModel : ViewModelBase
- {
- /// <summary>
- /// Список переходов
- /// </summary>
- readonly JumpList _jumpList;
- public MainViewModel()
- {
- // Инициализируем список и коллекцию
- _jumpList = new JumpList();
- JumpList.SetJumpList(Application.Current, _jumpList);
- _tasks = new ObservableCollection<TaskViewModel>();
- // Загружаем из файла задачи
- Load();
- // Обновляем список переходов
- Apply();
- // Команда обновления списка переходов
- ApplyCommand = new RelayCommand(Apply);
- }
- /// <summary>
- /// Команды
- /// </summary>
- public ICommand ApplyCommand { get; set; }
- /// <summary>
- /// Коллекция задач
- /// </summary>
- private ObservableCollection<TaskViewModel> _tasks;
- public ObservableCollection<TaskViewModel> Tasks
- {
- get { return _tasks; }
- set
- {
- if (_tasks != value)
- {
- _tasks = value;
- RaisePropertyChanged("Tasks");
- }
- }
- }
- /// <summary>
- /// Функция, перезаписывает список переходов.
- /// </summary>
- public void Apply()
- {
- // очистка существующего списка
- _jumpList.JumpItems.Clear();
- // получаем JumpTask
- var jumpTasks =
- (from task in Tasks.Select(x=>x.GetTask())
- where File.Exists(task.ApplicationPath)
- orderby task.Title, task.Category
- select new JumpTask
- {
- Title = task.Title ?? string.Empty,
- Description = task.Description ?? string.Empty,
- ApplicationPath = task.ApplicationPath ?? string.Empty,
- IconResourcePath = task.ApplicationPath ?? string.Empty,
- WorkingDirectory = Path.GetDirectoryName(task.ApplicationPath),
- CustomCategory = task.Category ?? string.Empty,
- }).ToList();
- // Шаманим с сортировкой. Этот код вообще не обязателен, просто мне нужен был список в определенном порядке
- jumpTasks.Reverse();
- // добавляем все JumpTask в список jumpTasks
- jumpTasks.ForEach(_jumpList.JumpItems.Add);
- // применяем изменения
- _jumpList.Apply();
- // сохраняем список в файл
- Save();
- }
- // Сохранение списка в файл
- public void Save()
- {
- Settings.Instance.SetTasks(Tasks.Select(x=>x.GetTask()));
- }
- // Загрузка списка из файла
- public void Load()
- {
- try
- {
- var tasks = Settings.Instance.GetTasks();
- foreach (var task in tasks.OrderBy(x=>x.Category).ThenBy(x=>x.Title))
- {
- _tasks.Add(new TaskViewModel(task));
- }
- }
- catch
- {
- _tasks = new ObservableCollection<TaskViewModel>();
- }
- }
- // Добавление нового элемента в список.
- public void Add(string fileName)
- {
- var task = new Task { ApplicationPath = fileName, Category = string.Empty, Title = Path.GetFileNameWithoutExtension(fileName) };
- _tasks.Add(new TaskViewModel(task));
- }
- }
Осталось только прибиндить это всё к гриду. Вот как я это сделал:
- <DataGrid x:Name="dg"
- Grid.Row="2" ItemsSource="{Binding Tasks, Mode=TwoWay}" AutoGenerateColumns="False" CanUserReorderColumns="True" CanUserResizeColumns="True"
- CanUserResizeRows="False" CanUserSortColumns="True" CanUserAddRows="True" CanUserDeleteRows="True" Margin="5" >
- <DataGrid.Columns>
- <DataGridTemplateColumn>
- <DataGridTemplateColumn.CellTemplate>
- <DataTemplate>
- <Image Source="{Binding Icon, Mode=OneWay}" Stretch="Uniform" Width="16"></Image>
- </DataTemplate>
- </DataGridTemplateColumn.CellTemplate>
- </DataGridTemplateColumn>
- <DataGridTextColumn Header="Category" Binding="{Binding Category, Mode=TwoWay}" Width="Auto"></DataGridTextColumn>
- <DataGridTextColumn Header="Title" Binding="{Binding Title, Mode=TwoWay}" Width="Auto"></DataGridTextColumn>
- <DataGridTextColumn Header="Description" Binding="{Binding Description, Mode=TwoWay}" Width="Auto"></DataGridTextColumn>
- <DataGridTemplateColumn Header="ApplicationPath" Width="*">
- <DataGridTemplateColumn.CellTemplate>
- <DataTemplate>
- <Button VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="2" CommandParameter="{Binding ApplicationPath}" Command="{Binding StartCommand}" >
- <Button.Background>
- <SolidColorBrush Color="Transparent"></SolidColorBrush>
- </Button.Background>
- <Button.Content>
- <TextBlock Text="{Binding ApplicationPath, Mode=OneWay}"></TextBlock>
- </Button.Content>
- </Button>
- </DataTemplate>
- </DataGridTemplateColumn.CellTemplate>
- </DataGridTemplateColumn>
- </DataGrid.Columns>
- </DataGrid>
Благодаря двухсторонней привязке, у меня есть возможность редактировать мой список задач. Ещё я добавил панель инструментов с одной кнопкой - для сохранения и обновления списка переходов.
- <ToolBar Grid.Row="1">
- <Button Command="{Binding ApplyCommand}">
- <Image Source="Images/apply.png" Height="16"/>
- </Button>
- </ToolBar>
Итак, теперь я могу редактировать мои задачи прямо в интерфейсе. Но я ещё не могу добавить их туда. Я решил использовать событие Drop главного окна, чтобы легко добавлять новые записи в мой список задач.
- private void WindowDrop(object sender, DragEventArgs e)
- {
- if (e.Data is DataObject && ((DataObject)e.Data).ContainsFileDropList())
- {
- foreach (string filePath in ((DataObject)e.Data).GetFileDropList())
- {
- if (File.Exists(filePath))
- _model.Add(filePath);
- }
- }
- }
В итоге, я получил приложение, которое позволяет синхронизировать мои списки переходов между всеми компьютерами. Также я предусмотрел, что если приложения, которое я указал на первом ПК, на втором ПК нет или путь к нему другой, - то ссылка на него на втором ПК не появится. Вот скрины моих результатов:
Теперь об ограничениях. Моя программа никак не работает с папками, она никак не работает с аргументами приложений, и вдобавок ей нужны именно *.exe файлы, ярлыки тоже никак не отработают. Меня все эти ограничения вполне устраивают, поэтому я ими не занимался.
Я было хотел выложить код программы на кодеплекс, но, как оказалось (и что, собственно, очевидно), использовать списки переходов для быстрого запуска программ идея не новая. Просто той функциональности, что была нужна мне, я в других программах не нашел, да и пощупать возможности панели задач уже давно чесались руки. Поэтому я свои исходники публиковать не буду (к тому же, я почти весь код в статье написал), а просто приведу несколько программ, которые решают подобные задачи.
Ещё ссылки:
- Общее описание списков переходов
- Windows 7: Как изменить количество элементов в Jump List
- JumpList MSDN
- О панели задач на Хабре
На этом всё. Всем спасибо.
UPD. Думаю, следует пояснить. В классе TaskViewModel команда StartCommand запускает приложение. Это я сделал только для того, чтобы у меня была возможность запустить приложение из моей программы. К списку переходов эта команда не имеет никакого отношения. Вся работа со списком переходов в классе MainViewModel
- /// <summary>
- /// Функция, перезаписывает список переходов.
- /// </summary>
- public void Apply()
- {
- // очистка существующего списка
- _jumpList.JumpItems.Clear();
- // получаем JumpTask
- var jumpTasks =
- (from task in Tasks.Select(x=>x.GetTask())
- where File.Exists(task.ApplicationPath)
- orderby task.Title, task.Category
- select new JumpTask
- {
- Title = task.Title ?? string.Empty,
- Description = task.Description ?? string.Empty,
- ApplicationPath = task.ApplicationPath ?? string.Empty,
- IconResourcePath = task.ApplicationPath ?? string.Empty,
- WorkingDirectory = Path.GetDirectoryName(task.ApplicationPath),
- CustomCategory = task.Category ?? string.Empty,
- }).ToList();
- // Шаманим с сортировкой. Этот код вообще не обязателен, просто мне нужен был список в определенном порядке
- jumpTasks.Reverse();
- // добавляем все JumpTask в список jumpTasks
- jumpTasks.ForEach(_jumpList.JumpItems.Add);
- // применяем изменения
- _jumpList.Apply();
- // сохраняем список в файл
- Save();
- }
Весь остальной код относится к работе непосредственно самой программы. Список переходов же доступен и тогда, когда программа не запущена.
UPD2. В комментариях попросили выложить исходники. Вот они.
Очень интересно!
ОтветитьУдалитьЕсть возможность запустить какое-нибудь приложение от администратора?
Конечно есть. Просто держи контрол+шифт, когда будешь запускать
ОтветитьУдалитьА, ясно. То есть это ограничение Windows для Jump списка.
ОтветитьУдалитьРешение интересное, только я не очень понял как осуществляется синхронизация между разными компьютерами.
ОтветитьУдалитьа про иконки файлов - я делал так.
Синхронизировать можно разными программами (Дропбокс, Windows Live Mesh). А за иконки спасибо, обязательно почитаю и попробую.
ОтветитьУдалитьА нельзя ли исходники выложить?
ОтветитьУдалитьСпасибо.
Конечно, Task Start
ОтветитьУдалить