From 48411d0f7932fd28c4b6ad7b8af76814a1fadec8 Mon Sep 17 00:00:00 2001 From: emoacht Date: Thu, 19 Nov 2015 03:54:10 +0900 Subject: [PATCH 1/3] Added flyout to notification area icon --- .../ViewModels/NotifyWindowViewModel.cs | 67 +++++ .../Views/NotifyIconComponent.cs | 72 ++++- WlanProfileViewer/Views/NotifyWindow.xaml | 98 +++++++ WlanProfileViewer/Views/NotifyWindow.xaml.cs | 122 ++++++++ WlanProfileViewer/Views/TaskbarAlignment.cs | 12 + WlanProfileViewer/Views/WindowPosition.cs | 264 ++++++++++++++++++ WlanProfileViewer/WlanProfileViewer.csproj | 10 + 7 files changed, 639 insertions(+), 6 deletions(-) create mode 100644 WlanProfileViewer/ViewModels/NotifyWindowViewModel.cs create mode 100644 WlanProfileViewer/Views/NotifyWindow.xaml create mode 100644 WlanProfileViewer/Views/NotifyWindow.xaml.cs create mode 100644 WlanProfileViewer/Views/TaskbarAlignment.cs create mode 100644 WlanProfileViewer/Views/WindowPosition.cs diff --git a/WlanProfileViewer/ViewModels/NotifyWindowViewModel.cs b/WlanProfileViewer/ViewModels/NotifyWindowViewModel.cs new file mode 100644 index 0000000..eb6d21d --- /dev/null +++ b/WlanProfileViewer/ViewModels/NotifyWindowViewModel.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Data; +using Reactive.Bindings; +using Reactive.Bindings.Extensions; +using Reactive.Bindings.Helpers; + +using WlanProfileViewer.Common; +using WlanProfileViewer.Views; + +namespace WlanProfileViewer.ViewModels +{ + public class NotifyWindowViewModel : BindableDisposableBase + { + private readonly MainWindow _mainWindow; + + private ReadOnlyReactiveCollection Profiles { get; } + + public ListCollectionView ProfilesView + { + get + { + if (_profilesView == null) + { + _profilesView = new ListCollectionView(Profiles); + _profilesView.Filter = x => ((ProfileItemViewModel)x).IsConnected.Value; + _profilesView.SortDescriptions.Add(new SortDescription(nameof(ProfileItemViewModel.InterfaceDescription), ListSortDirection.Ascending)); + } + + return _profilesView; + } + } + private ListCollectionView _profilesView; + + public ReadOnlyReactiveProperty IsAnyConnected { get; } + + public NotifyWindowViewModel(Window ownerWindow) + { + this._mainWindow = ownerWindow as MainWindow; + if (this._mainWindow == null) + throw new ArgumentException(nameof(ownerWindow)); + + var mainWindowViewModel = this._mainWindow.DataContext as MainWindowViewModel; + + this.Profiles = mainWindowViewModel.Profiles; + + IsAnyConnected = this.Profiles + .ObserveElementObservableProperty(x => x.IsConnected) + .ObserveOnUIDispatcher() + .Select(_ => + { + this.ProfilesView.Refresh(); + return (this.ProfilesView.Count > 0); + }) + .ToReadOnlyReactiveProperty() + .AddTo(this.Subscription); + + mainWindowViewModel.ReloadCommand.Execute(); + } + } +} \ No newline at end of file diff --git a/WlanProfileViewer/Views/NotifyIconComponent.cs b/WlanProfileViewer/Views/NotifyIconComponent.cs index 30a63fe..8d80b35 100644 --- a/WlanProfileViewer/Views/NotifyIconComponent.cs +++ b/WlanProfileViewer/Views/NotifyIconComponent.cs @@ -12,7 +12,7 @@ namespace WlanProfileViewer.Views public class NotifyIconComponent : Component { private readonly Window _ownerWindow; - private readonly IContainer _container; + private readonly Container _container; private readonly NotifyIcon _notifyIcon; public NotifyIconComponent(Window ownerWindow) @@ -22,7 +22,13 @@ public NotifyIconComponent(Window ownerWindow) this._ownerWindow = ownerWindow; this._container = new Container(); - this._notifyIcon = new NotifyIcon(this._container); + this._notifyIcon = new NotifyIcon(this._container) + { + ContextMenuStrip = new ContextMenuStrip() + }; + + _notifyIcon.MouseClick += OnMouseClick; + _notifyIcon.MouseDoubleClick += OnMouseDoubleClick; } #region Dispose @@ -73,6 +79,8 @@ public float Dpi #endregion + #region Icon + private void SetIcon(System.Drawing.Icon icon, float dpi) { if ((icon == null) || (dpi == 0F)) @@ -129,16 +137,36 @@ public void ShowIcon(System.Drawing.Icon icon, string text = null, float dpi = _ this.Dpi = dpi; _notifyIcon.Visible = true; - _notifyIcon.MouseClick += OnMouseClick; - _notifyIcon.MouseDoubleClick += OnMouseClick; } + #endregion + + #region Click + private void OnMouseClick(object sender, MouseEventArgs e) { - ShowWindow(); + if (e.Button == MouseButtons.Right) + { + ShowNotifyWindow(); + } + else + { + ShowOwnerWindow(); + } + } + + private void OnMouseDoubleClick(object sender, MouseEventArgs e) + { + ShowOwnerWindow(); } - private void ShowWindow() + private void ShowNotifyWindow() + { + var window = new NotifyWindow(_ownerWindow, GetClickedPoint()); + window.Show(); + } + + private void ShowOwnerWindow() { if (_ownerWindow.WindowState == WindowState.Minimized) { @@ -152,5 +180,37 @@ private void ShowWindow() _ownerWindow.WindowState = WindowState.Minimized; } } + + /// + /// Get the point where NotifyIcon is clicked by the position of ContextMenuStrip and NotifyIcon. + /// + /// Cursor location + /// MouseEventArgs.Location property of MouseClick event does not contain data. + private Point GetClickedPoint() + { + var contextMenuStrip = _notifyIcon.ContextMenuStrip; + + var corners = new Point[] + { + //new Point(contextMenuStrip.Left, contextMenuStrip.Top), + //new Point(contextMenuStrip.Right, contextMenuStrip.Top), + new Point(contextMenuStrip.Left, contextMenuStrip.Bottom), + new Point(contextMenuStrip.Right, contextMenuStrip.Bottom) + }; + + var notifyIconRect = WindowPosition.GetNotifyIconRect(_notifyIcon); + if (notifyIconRect != Rect.Empty) + { + foreach (var corner in corners) + { + if (notifyIconRect.Contains(corner)) + return corner; + } + } + + return corners.Last(); // Fallback + } + + #endregion } } \ No newline at end of file diff --git a/WlanProfileViewer/Views/NotifyWindow.xaml b/WlanProfileViewer/Views/NotifyWindow.xaml new file mode 100644 index 0000000..f66cd23 --- /dev/null +++ b/WlanProfileViewer/Views/NotifyWindow.xaml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WlanProfileViewer/Views/NotifyWindow.xaml.cs b/WlanProfileViewer/Views/NotifyWindow.xaml.cs new file mode 100644 index 0000000..a7a1e82 --- /dev/null +++ b/WlanProfileViewer/Views/NotifyWindow.xaml.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +using WlanProfileViewer.ViewModels; + +namespace WlanProfileViewer.Views +{ + public partial class NotifyWindow : Window + { + private readonly Window _ownerWindow; + private readonly Point _pivotLocation; + private readonly TaskbarAlignment _taskbarAlignment; + + public NotifyWindow() : this(null, default(Point)) + { } + + public NotifyWindow(Window ownerWindow, Point pivotLocation) + { + InitializeComponent(); + + this.Topmost = true; + this.ShowInTaskbar = false; + + // Assigning ownerWindow to this.Owner will bring the owner window at the top (not the topmost) + // when this window is activated. Such behavior is not desirable. + this._ownerWindow = ownerWindow; + + if (this._ownerWindow != null) + this._ownerWindow.Closing += OnOwnerWindowClosing; + + this._pivotLocation = pivotLocation; + this._taskbarAlignment = WindowPosition.GetTaskbarAlignment(); + + this.DataContext = new NotifyWindowViewModel(ownerWindow); + } + + protected override Size ArrangeOverride(Size arrangeBounds) + { + SetWindowLocation(arrangeBounds); + + return base.ArrangeOverride(arrangeBounds); + } + + private void SetWindowLocation(Size windowSize) + { + var left = _pivotLocation.X; + var top = _pivotLocation.Y; + + switch (_taskbarAlignment) + { + case TaskbarAlignment.Left: + // Place this window at the top-right of the pivot. + left += 1; + top += -windowSize.Height - 1; + break; + + case TaskbarAlignment.Top: + // Place this window at the bottom-left of the pivot. + left += -windowSize.Width - 1; + top += 1; + break; + + case TaskbarAlignment.Right: + case TaskbarAlignment.Bottom: + // Place this window at the top-left of the pivot. + left += -windowSize.Width - 1; + top += -windowSize.Height - 1; + break; + + default: + return; + } + + if ((this.Left == left) && (this.Top == top)) + return; + + WindowPosition.SetWindowLocation(this, new Point(left, top)); + } + + #region Close + + private bool _isClosing = false; + + protected override void OnClosing(CancelEventArgs e) + { + _isClosing = true; + + if (_ownerWindow != null) + _ownerWindow.Closing -= OnOwnerWindowClosing; + + base.OnClosing(e); + } + + private void OnOwnerWindowClosing(object sender, CancelEventArgs e) + { + if (!_isClosing) + this.Close(); + } + + protected override void OnDeactivated(EventArgs e) + { + if (!_isClosing) + this.Close(); + + //base.OnDeactivated(e); + } + + #endregion + } +} \ No newline at end of file diff --git a/WlanProfileViewer/Views/TaskbarAlignment.cs b/WlanProfileViewer/Views/TaskbarAlignment.cs new file mode 100644 index 0000000..3c62991 --- /dev/null +++ b/WlanProfileViewer/Views/TaskbarAlignment.cs @@ -0,0 +1,12 @@ + +namespace WlanProfileViewer.Views +{ + public enum TaskbarAlignment + { + None = 0, + Left, + Top, + Right, + Bottom, + } +} \ No newline at end of file diff --git a/WlanProfileViewer/Views/WindowPosition.cs b/WlanProfileViewer/Views/WindowPosition.cs new file mode 100644 index 0000000..0ca8c4f --- /dev/null +++ b/WlanProfileViewer/Views/WindowPosition.cs @@ -0,0 +1,264 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Forms; +using System.Windows.Interop; + +namespace WlanProfileViewer.Views +{ + public static class WindowPosition + { + #region Win32 + + [DllImport("User32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetWindowPos( + IntPtr hWnd, + IntPtr hWndInsertAfter, + int X, + int Y, + int cx, + int cy, + SWP uFlags); + + private enum SWP : uint + { + SWP_ASYNCWINDOWPOS = 0x4000, + SWP_DEFERERASE = 0x2000, + SWP_DRAWFRAME = 0x0020, + SWP_FRAMECHANGED = 0x0020, + SWP_HIDEWINDOW = 0x0080, + SWP_NOACTIVATE = 0x0010, + SWP_NOCOPYBITS = 0x0100, + SWP_NOMOVE = 0x0002, + SWP_NOOWNERZORDER = 0x0200, + SWP_NOREDRAW = 0x0008, + SWP_NOREPOSITION = 0x0200, + SWP_NOSENDCHANGING = 0x0400, + SWP_NOSIZE = 0x0001, + SWP_NOZORDER = 0x0004, + SWP_SHOWWINDOW = 0x0040, + } + + [DllImport("Shell32.dll", SetLastError = true)] + private static extern IntPtr SHAppBarMessage( + ABM dwMessage, + ref APPBARDATA pData); + + [StructLayout(LayoutKind.Sequential)] + private struct APPBARDATA + { + public uint cbSize; + public IntPtr hWnd; + public uint uCallbackMessage; + public ABE uEdge; + public RECT rc; + public int lParam; + } + + [StructLayout(LayoutKind.Sequential)] + private struct RECT + { + public int left; + public int top; + public int right; + public int bottom; + + public static implicit operator Rect(RECT rect) + { + if ((rect.right - rect.left < 0) || (rect.bottom - rect.top < 0)) + return Rect.Empty; + + return new Rect( + rect.left, + rect.top, + rect.right - rect.left, + rect.bottom - rect.top); + } + } + + private enum ABM : uint + { + ABM_NEW = 0x00000000, + ABM_REMOVE = 0x00000001, + ABM_QUERYPOS = 0x00000002, + ABM_SETPOS = 0x00000003, + ABM_GETSTATE = 0x00000004, + ABM_GETTASKBARPOS = 0x00000005, + ABM_ACTIVATE = 0x00000006, + ABM_GETAUTOHIDEBAR = 0x00000007, + ABM_SETAUTOHIDEBAR = 0x00000008, + ABM_WINDOWPOSCHANGE = 0x00000009, + ABM_SETSTATE = 0x0000000A, + } + + private enum ABE : uint + { + ABE_LEFT = 0, + ABE_TOP = 1, + ABE_RIGHT = 2, + ABE_BOTTOM = 3 + } + + [DllImport("Shell32.dll", SetLastError = true)] + private static extern int Shell_NotifyIconGetRect( + [In] ref NOTIFYICONIDENTIFIER identifier, + out RECT iconLocation); + + [StructLayout(LayoutKind.Sequential)] + private struct NOTIFYICONIDENTIFIER + { + public uint cbSize; + public IntPtr hWnd; + public uint uID; + public GUID guidItem; // System.Guid can be used. + } + + [StructLayout(LayoutKind.Sequential)] + private struct GUID + { + public uint Data1; + public ushort Data2; + public ushort Data3; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public byte[] Data4; + } + + private const int S_OK = 0x00000000; + private const int S_FALSE = 0x00000001; + + #endregion + + #region Window + + public static bool SetWindowLocation(Window window, Point location) + { + var windowHandle = new WindowInteropHelper(window).Handle; + + return SetWindowPos( + windowHandle, + new IntPtr(-1), // HWND_TOPMOST + (int)location.X, + (int)location.Y, + 0, + 0, + SWP.SWP_NOSIZE); + } + + #endregion + + #region Taskbar + + public static TaskbarAlignment GetTaskbarAlignment() + { + var data = new APPBARDATA + { + cbSize = (uint)Marshal.SizeOf(typeof(APPBARDATA)), + }; + + var result = SHAppBarMessage( + ABM.ABM_GETTASKBARPOS, + ref data); + + return (result != IntPtr.Zero) + ? ConvertToTaskbarAlignment(data.uEdge) + : TaskbarAlignment.None; + } + + private static TaskbarAlignment ConvertToTaskbarAlignment(ABE value) + { + switch (value) + { + case ABE.ABE_LEFT: + return TaskbarAlignment.Left; + case ABE.ABE_TOP: + return TaskbarAlignment.Top; + case ABE.ABE_RIGHT: + return TaskbarAlignment.Right; + case ABE.ABE_BOTTOM: + return TaskbarAlignment.Bottom; + default: + throw new NotSupportedException("The value is unknown."); + } + } + + #endregion + + #region NotifyIcon + + /// + /// Get the rectangle of a specified NotifyIcon. + /// + /// NotifyIcon + /// Rectangle of the NotifyIcon + /// + /// The idea to get the rectangle of a NotifyIcon is derived from: + /// https://github.com/rzhw/SuperNotifyIcon + /// + public static Rect GetNotifyIconRect(NotifyIcon notifyIcon) + { + NOTIFYICONIDENTIFIER identifier; + if (!TryGetNotifyIconIdentifier(notifyIcon, out identifier)) + return Rect.Empty; + + RECT iconLocation; + int result = Shell_NotifyIconGetRect(ref identifier, out iconLocation); + + Debug.WriteLine($"Shell_NotifyIconGetRect: {result}"); + + switch (result) + { + case S_OK: + case S_FALSE: + return iconLocation; + default: + return Rect.Empty; + } + } + + private static bool TryGetNotifyIconIdentifier(NotifyIcon notifyIcon, out NOTIFYICONIDENTIFIER identifier) + { + identifier = new NOTIFYICONIDENTIFIER() + { + cbSize = (uint)Marshal.SizeOf(typeof(NOTIFYICONIDENTIFIER)) + }; + + int id; + if (!TryGetNonPublicFieldValue(notifyIcon, "id", out id)) + return false; + + NativeWindow window; + if (!TryGetNonPublicFieldValue(notifyIcon, "window", out window)) + return false; + + identifier.uID = (uint)id; + identifier.hWnd = window.Handle; + return true; + } + + private static bool TryGetNonPublicFieldValue(object instance, string fieldName, out T fieldValue) + { + fieldValue = default(T); + + var fieldInfo = instance.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); + if (fieldInfo == null) + return false; + + var value = fieldInfo.GetValue(instance); + if (!(value is T)) + return false; + + fieldValue = (T)value; + return true; + } + + #endregion + } +} \ No newline at end of file diff --git a/WlanProfileViewer/WlanProfileViewer.csproj b/WlanProfileViewer/WlanProfileViewer.csproj index 56e4757..0ed6a4c 100644 --- a/WlanProfileViewer/WlanProfileViewer.csproj +++ b/WlanProfileViewer/WlanProfileViewer.csproj @@ -115,6 +115,9 @@ + + + @@ -146,6 +149,9 @@ Component + + NotifyWindow.xaml + @@ -155,6 +161,10 @@ MainWindow.xaml Code + + Designer + MSBuild:Compile + Designer MSBuild:Compile From 63ad3d357701110850e767d5920eb6aec104eeda Mon Sep 17 00:00:00 2001 From: emoacht Date: Thu, 19 Nov 2015 03:54:42 +0900 Subject: [PATCH 2/3] Incremented build number --- WlanProfileViewer/Properties/AssemblyInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WlanProfileViewer/Properties/AssemblyInfo.cs b/WlanProfileViewer/Properties/AssemblyInfo.cs index dbaa559..77e4766 100644 --- a/WlanProfileViewer/Properties/AssemblyInfo.cs +++ b/WlanProfileViewer/Properties/AssemblyInfo.cs @@ -51,5 +51,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.1.0.4")] -[assembly: AssemblyFileVersion("1.1.0.4")] +[assembly: AssemblyVersion("1.1.1.6")] +[assembly: AssemblyFileVersion("1.1.1.6")] From b1f872d066fd3a1db0030a917925cc767492d144 Mon Sep 17 00:00:00 2001 From: emoacht Date: Thu, 19 Nov 2015 03:57:50 +0900 Subject: [PATCH 3/3] Updated readme --- History.md | 4 ++++ README.md | 2 +- README_ja.md | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/History.md b/History.md index eb6ca78..6e49b3b 100644 --- a/History.md +++ b/History.md @@ -1,5 +1,9 @@ ##History +Ver 1.1.1 2015-11-18 + + - Added flyout to notification area icon + Ver 1.1.0 2015-11-13 - Switched from internal NativeWifi to ManagedNativeWifi diff --git a/README.md b/README.md index 3a5e090..9a6b10e 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ WLAN Profile Viewer is a Windows desktop app to manage wireless LAN profiles. It ##Download -[Download](https://github.com/emoacht/WlanProfileViewer/releases/download/1.1.0/WlanProfileViewer110.zip) +[Download](https://github.com/emoacht/WlanProfileViewer/releases/download/1.1.1/WlanProfileViewer111.zip) ##Install diff --git a/README_ja.md b/README_ja.md index 3bf4ce1..200cb26 100644 --- a/README_ja.md +++ b/README_ja.md @@ -19,7 +19,7 @@ WLAN Profile Viewerは無線LANプロファイルを管理するためのWindows ##ダウンロード -[ダウンロード](https://github.com/emoacht/WlanProfileViewer/releases/download/1.1.0/WlanProfileViewer110.zip) +[ダウンロード](https://github.com/emoacht/WlanProfileViewer/releases/download/1.1.1/WlanProfileViewer111.zip) ##インストール