##まとめ
いい加減終わらせないと気持ちが悪いし、ここまで進めてきて、機能的にはほぼ実装が完了したのでこれにて終了することにしました。
【できたこと】
・元のアプリの機能を実装する
・当初目標である画面解像度によりフォームのボケについてはおおむね解消
・追加機能としていたアプリのアイコン化表示モードについても実装
【できなかったこと・わからなかったこと】
・画像化リソースを利用している部分については、結局ぼけたりしてる
・DragMoveでのWindowのちらつき
・起動中の解像度変更への対応、解像度の異なるマルチディスプレイへの移動時の自動対応
左から、Forms版(画面解像度125%)WPF版(画面解像度125%)、WPF版(画面解像度100%)。
いろいろトピックスまとめると、、
#NotifyIconの表示
・WPF NotifyIconを利用
・NuGetで上記のHardcodet.NotifyIcon.Wpfを導入
(プロジェクトメニューからNuGetパッケージの管理を選択し、参照タブでHardcodet.NotifyIconと入れると出てきます)
・デザイナで作業する場合、Hardcodet.NotifyIcon.Wpfの導入で追加される「TaskbarIcon」をウインドウ上の好きな場所に配置
・詳しくは上記サイトのチュートリアルを参考。
・固定アイコンならICONリソースや画像ファイルからデータをセット
・アイコンに対してのイベントは「Trayxxx」で記述
・ICONリソースを出すだけなら以下でOK
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
//notify icon 表示(コントロール名はNotifyIcon)
this.NotifyIcon.Icon = Properties.Resources.notify_static; //notify_static.ico という名前でリソースに取り込まれている例
this.NotifyIcon.Visibility = Visibility.visible; //最初は表示にする
}
#Timerの作成
System.Windows.Threadingを利用。DispatcherTimerとして新規タイマーを定義し、TimeSpanでインターバルを設定。
設定したタイマーに定期的に呼び出したい処理をイベント設定する。
設定ができたらタイマーをStartする。
using System.Windows.Threading;
//・・・
public partial class MainWindow : Window
{
private DispatcherTimer MyTimer; //Timer宣言
public MainWindow()
{
//メインWINDOW処理
InitializeComponent();
MyTimer = new DispatcherTimer //TIMERの作成
{
Interval = new TimeSpan(0, 0, 1) //1秒毎タイマー
};
MyTimer.Tick += new EventHandler(MainLoop); //タイマーイベントにMainLoopを設定
MyTimer.Start(); //TIMERの開始
}
private void MainLoop(object sender, EventArgs e)
{
//do something・・タイマーで実行したいことを記述
}
//・・・
}
#NotifyIconの動的な更新
(7)でのロジックを変更。当初Window上に描画したCanvasをそのままNotifyIconに入れてましたがNotifyIcon用には別にメモリ上にCanvasを作成してそちらを入れてあげてます。最終的に以下のように変更しました
本体側CS↓
public void DrawCanvas()
{
//Window上の物理Canvasにいろいろ描画する
//またNotifyIcon用にメモリ上にCanvasを作成しそちらにも描画する
//描画自体はDrawProc.CS側で実行
Canvas iconCanvas = new Canvas();
iconCanvas.Width = 16 * G.SR;
iconCanvas.Height = 16 * G.SR;
//毎秒処理
switch (G.Pref.Notifyicon)
{
case 1: //menu_static
DrawProc.DrawStatic(titleCanvas, G.MODE);
DrawProc.DrawStatic(iconCanvas, 0);
SetIcon(iconCanvas);
break;
case 2://menu_digtal
DrawProc.DrawDigitalSec(titleCanvas, G.MODE);
DrawProc.DrawDigitalSec(iconCanvas, 0);
SetIcon(iconCanvas);
break;
case 3://menu_gothic
DrawProc.DrawGothic(titleCanvas, G.MODE);
DrawProc.DrawGothic(iconCanvas, 0);
SetIcon(iconCanvas);
break;
//以下略
}
}
public void SetIcon(Canvas aCanvas)
{
//Canvasを受け取り、画像化してNotifyIconにセット
aCanvas.Arrange(new Rect(aCanvas.RenderSize));
aCanvas.Measure(aCanvas.RenderSize);
var bounds = VisualTreeHelper.GetDescendantBounds(aCanvas);
var RTbitmap = new RenderTargetBitmap((int)bounds.Width, (int)bounds.Height, 96.0d, 96.0d, PixelFormats.Pbgra32);
var dv = new DrawingVisual();
using (var dc = dv.RenderOpen())
{
var vb = new VisualBrush(aCanvas);
dc.DrawRectangle(vb, null, bounds);
}
RTbitmap.Render(dv);
RTbitmap.Freeze();
//RenderTargetBitmap => bitmap
var bitmap = new System.Drawing.Bitmap((int)bounds.Width, (int)bounds.Height, System.Drawing.Imaging.PixelFormat.Format32bppPArgb);
var bitmapData = bitmap.LockBits(new System.Drawing.Rectangle(System.Drawing.Point.Empty, bitmap.Size), System.Drawing.Imaging.ImageLockMode.WriteOnly, bitmap.PixelFormat);
RTbitmap.CopyPixels(Int32Rect.Empty, bitmapData.Scan0, bitmapData.Stride * bitmapData.Height, bitmapData.Stride);
//bitmap =>iconHandler => icon
bitmap.UnlockBits(bitmapData);
IntPtr Hicon = bitmap.GetHicon();
System.Drawing.Icon icon = System.Drawing.Icon.FromHandle(Hicon);
NotifyIcon.Icon = icon;
bitmap.Dispose();
NativeMethods.DestroyIcon(icon.Handle);
}
#Prefの作成・保存・読み込み
定番のXML形式で設定を記述することとしそれ用のPrefクラスを作成してそこのREADとSAVEをまとめた例。保存先は固定でドキュメントフォルダ直下としています。(Prefをドキュメントフォルダってのも安直ですが)
public class Pref
{
private int _lang ; //言語
//・・・中略
private int _mode2size; //アイコン化時のサイズ
public int Lang
{
get { return _lang; }
set { _lang = value; }
}
//・・・中略
public int Mode2size
{
get { return _mode2size; }
set { _mode2size = value; }
}
public Pref()
{
_lang = 0;
//・・・中略
_mode2size = 48;
}
public void Read()
{
//設定ファイルの読み込み
var PrefPath = System.Environment.GetFolderPath(Environment.SpecialFolder.Personal) + "\\BootRecorder.pref";
if (System.IO.File.Exists(PrefPath))
{
//PrefFileが存在する場合のみ、読み込んでGlobalに上書きセット
System.Xml.Serialization.XmlSerializer serializer = new System.Xml.Serialization.XmlSerializer(typeof(Pref));
System.IO.StreamReader sr = new System.IO.StreamReader(PrefPath, new System.Text.UTF8Encoding(false));
G.Pref = (Pref)serializer.Deserialize(sr);
sr.Close();
}
}
public void Save(Window win)
{
//設定のファイルへの保存
var PrefPath = System.Environment.GetFolderPath(Environment.SpecialFolder.Personal) + "\\BootRecorder.pref";
System.Xml.Serialization.XmlSerializer serializer = new System.Xml.Serialization.XmlSerializer(typeof(Pref));
try
{
System.IO.StreamWriter sw = new System.IO.StreamWriter(PrefPath, false, new System.Text.UTF8Encoding(false));
serializer.Serialize(sw, G.Pref);
sw.Close();
}
catch (System.IO.IOException e)
{
MessageBox.Show("Pref File is in use with another application." + "\r\n" + e.Message
, "BootRecorder", MessageBoxButton.OK, MessageBoxImage.Error);
win.Close();
}
}
}
#テキストファイルの作成・読み込み
CSV形式のログファイルをStreamで作成、リードしています。
取り込みはまとめてBuffに格納し、後の処理 MakeListView で行に分解していきます。
private void ReadLog()
{
//ログファイルの読み込み
//ファイル名作成
if (G.Pref.LogPath == "") G.Pref.LogPath = System.Environment.GetFolderPath(Environment.SpecialFolder.Personal) +
"\\BootLog_" + DateTime.Now.Year.ToString() + ".log";
//存在確認してなければ作成
if (File.Exists(G.Pref.LogPath) == false) CreateLog();
//読む
string buff="";
Encoding sjisEnc = Encoding.GetEncoding("Shift_JIS");
try
{
StreamReader sr = new StreamReader(G.Pref.LogPath, sjisEnc);
buff = sr.ReadToEnd();
sr.Close();
}
catch (System.IO.IOException e)
{
MessageBox.Show("log file read error." + "\r\n" + e.Message
, "BootRecorder", MessageBoxButton.OK, MessageBoxImage.Error);
Close();
}
//ListViewへセット
MakeListView(buff);
}
private void CreateLog()
{
//ログファイルの新規作成
string buff = "";
DateTime dt = new DateTime(DateTime.Now.Year, 1, 1);
while (DateTime.Now.Year == dt.Year) //今年の全日ループ
{
string sdate = dt.ToShortDateString();
buff = buff + sdate.Replace("/", "") + "," + (int)dt.DayOfWeek + ",,";
dt = dt.AddDays(1);
if (DateTime.Now.Year != dt.Year) break;
buff = buff + "\r\n";
}
//書き出し
Encoding sjisEnc = Encoding.GetEncoding("Shift_JIS");
try
{
StreamWriter sw = new StreamWriter(G.Pref.LogPath, false, sjisEnc);
sw.WriteLine(buff);
sw.Close();
}
catch (System.IO.IOException e)
{
MessageBox.Show("log file write error." + "\r\n" + e.Message
, "BootRecorder", MessageBoxButton.OK, MessageBoxImage.Error);
Close();
}
}
#テキストからListViewの作成
ReadLogで作成されたBuffを受け取って、改行で行分割、さらにカンマでフィールド分割し、曜日を文字列変換して専用クラスに格納し、ListViewにAddしていきます。
private void MakeListView(string Buff)
{
//ListViewを作成
//ListViewのクリア
listView.Items.Clear();
//曜日
string[] e_w = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"};
//行配列化、さらにフィールド分解してListViewItemとして追加
string[] crlf = { "\r\n" };
string[] lines = Buff.Split(crlf, StringSplitOptions.None);
string[] dlm = { "," };
for (int i = 0; i < lines.Length-1; i++)
{
string[] fld = lines[i].Split(dlm, StringSplitOptions.None);
BootRec BootRec = new BootRec
{
date = fld[0].Substring(4, 2) + "/" + fld[0].Substring(6, 2),
dow = e_w[int.Parse(fld[1])],
on = fld[2],
off = fld[3]
};
listView.Items.Add(BootRec);
}
listView.Items.Refresh();
}
#ListViewとログの更新
当ツールのメイン機能であるログの更新処理です。1分ごとに呼びだされ、現在の時分(HHMM)でウインドウ上ListViewの該当日付の起動時化及び終了時刻を更新し続け、毎回、更新後のListViewのアイテムをもとにログファイルを再作成することでログの更新も行います。
private void UpdateListviewAndLog()
{
//ログの更新
DateTime dt = DateTime.Now;
string hhMM = dt.ToString("HHmm");
string yyyyMMdd = "";
string MMdd = "";
if(int.Parse(hhMM) < int.Parse(G.Pref.Daybreak))
{
hhMM = (int.Parse(hhMM) + 2400).ToString();
dt = dt.AddDays(-1);
}
if(dt.Year != DateTime.Now.Year)
{
//日付変更日時により前年のログファイルが要求された
MessageBox.Show("Fail to update log ! With date-change-time settings , it refers to last-year-log .",
"BootRecorder", MessageBoxButton.OK, MessageBoxImage.Error);
Close();
}
else
{
//Listview表示形式に加工
yyyyMMdd = dt.ToString("yyyyMMdd");
MMdd = dt.ToString("MM/dd");
hhMM = hhMM.Substring(0, 2) + ":" + hhMM.Substring(2, 2);
}
string buff = "";
//Listview上で該当日時のアイテムの更新しつつbuffを再作成
foreach (BootRec br in listView.Items)
{
if (br.date == MMdd)
{
if (br.on == "") br.on = hhMM;
br.off = hhMM;
}
DateTime dt2 = DateTime.Parse(dt.Year + "/" + br.date);
string sdate = dt2.ToShortDateString();
buff = buff + sdate.Replace("/", "") + "," + (int)dt2.DayOfWeek + "," + br.on + "," + br.off;
if (br.date == "12/31") break;
buff = buff + "\r\n";
}
listView.Items.Refresh();
//buffからログを再書き出し
//書き出し
Encoding sjisEnc = Encoding.GetEncoding("Shift_JIS");
try
{
StreamWriter sw = new StreamWriter(G.Pref.LogPath, false, sjisEnc);
sw.WriteLine(buff);
sw.Close();
}
catch (System.IO.IOException e)
{
MessageBox.Show("log file update error." + "\r\n" + e.Message
, "BootRecorder", MessageBoxButton.OK, MessageBoxImage.Error);
Close();
}
}
#ListViewのスタイル制御
リスト内のスタイルをデータに応じて変化させる場合、XAML上でStyleにTrigger設定しDataTriggerでバインド対象となる項目名とデータ内容条件を設定しておきます。またセレクトによるスタイルの制御もTrigger Property="IsSelected" Value="True" により可能。コード側は特に何も記述しなくてもOK。
<Style.Triggers>
<DataTrigger Binding="{Binding dow}" Value="SUN"> ←項目dowがSUNの場合:日曜日
<Setter Property="Foreground" Value="Red"/> ←前景色を赤に
</DataTrigger>
<DataTrigger Binding="{Binding dow}" Value="SAT"> ←項目dowがSATの場合:土曜日
<Setter Property="Foreground" Value="Blue" /> ←前景色を青に
</DataTrigger>
<Trigger Property="IsSelected" Value="True" >
<Setter Property="Background" Value="{x:Static SystemColors.HighlightBrush}" />
<Setter Property="Foreground" Value="{x:Static SystemColors.HighlightTextBrush}" />
</Trigger>
</Style.Triggers>
#コンテキストメニューの作り方・処理の呼び出し
デザイナでベースを作る場合の手順は、
①デザイナで、CMSを設定したいコントロール上のプロパティ内の「その他の指定」内のContextMenuで新規作成ボタンを押す
②ContextMenuのプロパティが下に展開表示されるので、その中のItemsの右の「...」ボタンを押す
③コレクションエディタが表示されるので、普通のメニューアイテムであれば下のプルダウンを「MenuItems」にして追加ボタンを押す
④追加されたMenuItemのプロパティが右側に表示されるので、「Header」にメニュー表示文字列を入力する。
⑤アイテム選択ごとに起動するルーチンはXAML上で適宜定義できる。同一ルーチンにまとめて処理分岐させる場合はTagなどを使ってコード側で判定し、適切な処理に分岐させる
という方法を取りました。
NotifyIconの種類を選ばせるCMS XMAL側の記述↓
<Image x:Name="menu_notify_icon" Height="16" OpacityMask="#FF99B4D1" Width="16" Stretch="Fill" SnapsToDevicePixels="True" Margin="82,72,92.4,0" VerticalAlignment="Top" d:LayoutOverrides="Height">
<Image.ContextMenu>
<ContextMenu FontSize="9" FontFamily="Meiryo" IsManipulationEnabled="True">
<MenuItem Header="static icon" Click="MenuItem_Click" Tag="1">
<MenuItem.Icon>
<Image Source="menu_static.bmp" Width="16" Height="16" Stretch="None" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="sec. in digital font" Click="MenuItem_Click" Tag="2">
<MenuItem.Icon>
<Image Source="menu_digtal.bmp" Width="16" Height="16" Stretch="None" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="sec. in gothic font" Click="MenuItem_Click" Tag="3">
<MenuItem.Icon>
<Image Source="menu_gothic.bmp" Width="16" Height="16" Stretch="None" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="analog clock" Click="MenuItem_Click" Tag="4">
<MenuItem.Icon>
<Image Source="menu_analog.bmp" Width="16" Height="16" Stretch="None" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="sec. in analog" Click="MenuItem_Click" Tag="5">
<MenuItem.Icon>
<Image Source="menu_anasec.bmp" Width="16" Height="16" Stretch="None" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="3 digit counter" Click="MenuItem_Click" Tag="6">
<MenuItem.Icon>
<Image Source="menu_digit3.bmp" Width="16" Height="16" Stretch="None" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</Image.ContextMenu>
</Image>
C#処理側記述↓
アイコンの選択処理は設定画面からだけでなくノティファイアイコンのCMSメニューからも呼び出しで使うので、ルーチンにくくりだしています。
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
//設定画面上のアイコンセレクトコンテキストメニューの選択
MenuItem selectedItem = (MenuItem)sender;
SelectIcon(selectedItem.Tag.ToString(), selectedItem.Header.ToString());
}
private void SelectIcon(string Tag, string Header)
{
//アイコン変更
var bitmap = Properties.Resources.menu_static;
switch (Tag)
{
case "1":
bitmap = Properties.Resources.menu_static;
G.Pref.Notifyicon = 1;
break;
case "2":
bitmap = Properties.Resources.menu_digtal;
G.Pref.Notifyicon = 2;
break;
case "3":
bitmap = Properties.Resources.menu_gothic;
G.Pref.Notifyicon = 3;
break;
case "4":
bitmap = Properties.Resources.menu_analog;
G.Pref.Notifyicon = 4;
break;
case "5":
bitmap = Properties.Resources.menu_anasec;
G.Pref.Notifyicon = 5;
break;
case "6":
bitmap = Properties.Resources.menu_digit3;
G.Pref.Notifyicon = 6;
G.CNT = 0;
break;
default:
bitmap = Properties.Resources.menu_static;
break;
}
IntPtr hbitmap = bitmap.GetHbitmap();
menu_notify_icon.Source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(hbitmap, IntPtr.Zero, Int32Rect.Empty, System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions());
icon_name.Content = Header;
}
#Click透過設定
Windowのクリック透過を実装することで、アナログ時計をガジェット的にクリック反応しない背景として表示しっぱなしにしたいなあということで実装しました
User32系をまとめたCS側でGet/SetWindowLongを定義↓
internal static class NativeMethods
{
[DllImport("user32.dll")]
public static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwLong);
//中略
}
internal static class Consts
{
//中略
public const int GWL_EXSTYLE = (-20);
public const int WS_EX_TRANSPARENT = 0x00000020;
public const int WS_EX_TOPMOST = 0x00000008;
}
メイン側の記述でクリック受けと無視時の値を保存しておき↓
protected override void OnSourceInitialized(EventArgs e)
{
//Icon Mode時のクリックEventの受け・スルー切り替え
base.OnSourceInitialized(e);
//WindowHandle(Win32) を取得
var handle = new WindowInteropHelper(this).Handle;
//クリックをキャッチする場合の値を保存
int extendStyle = NativeMethods.GetWindowLong(handle, Consts.GWL_EXSTYLE);
if (Topmost == true) extendStyle ^= Consts.WS_EX_TOPMOST; //TOP MOST分減算
G.catch_extendStyle = extendStyle;
//クリックをスルー場合に値を保存
extendStyle |= Consts.WS_EX_TRANSPARENT; //フラグの追加
if (Topmost == true) extendStyle ^= Consts.WS_EX_TOPMOST; //TOP MOST分減算
G.through_extendStyle = extendStyle;
//初期はキャッチで設定
NativeMethods.SetWindowLong(handle, Consts.GWL_EXSTYLE, G.catch_extendStyle);
}
NotifyIconのCMSメニューでの設定変更の際に、TopMostを考慮の上設定を行ってます↓
private void NotifyIcon_MenuItem_Click(object sender, RoutedEventArgs e)
{
//ノティファイアイコン上でのコンテキストメニュー全般の処理
MenuItem selectedItem = (MenuItem)sender;
switch (selectedItem.Tag.ToString())
{
//中略
case "8": //Icon Mode click ignore on/off
if (G.MODE == 2)
{
var handle = new WindowInteropHelper(this).Handle;
int extendStyle = NativeMethods.GetWindowLong(handle, Consts.GWL_EXSTYLE);
if (Topmost == true) extendStyle ^= Consts.WS_EX_TOPMOST;
if (extendStyle == G.catch_extendStyle)
{
NativeMethods.SetWindowLong(handle, Consts.GWL_EXSTYLE, G.through_extendStyle);
IgnoreEvent.IsChecked = true;
}
else
{
NativeMethods.SetWindowLong(handle, Consts.GWL_EXSTYLE, G.catch_extendStyle);
IgnoreEvent.IsChecked = false;
}
}
else IgnoreEvent.IsChecked = false;
break;
//中略
}
}
#常駐設定・解除
(14)で書いた通りで実装。
private void btn_SetStartup_Click(object sender, RoutedEventArgs e)
{
//スタートアップに自身のショートカットを作成する
//WshShellを作成
var t = Type.GetTypeFromCLSID(new Guid("72C24DD5-D70A-438B-8A42-98424B88AFB8"));
dynamic shell = Activator.CreateInstance(t);
//ショートカット作成先(startupフォルダパス+ショートカット名)
var shortcutPath = Environment.GetFolderPath(Environment.SpecialFolder.StartMenu) + "\\Programs\\Startup\\Bootrecorder.lnk";
//実行ファイルパス(なるべくならFromsは使いたくないのでAssenblyを利用)
Assembly myAssembly = Assembly.GetEntryAssembly();
string thisAppPath = myAssembly.Location;
//ショートカットを指定先に作成
object shortcut = t.InvokeMember("CreateShortcut", System.Reflection.BindingFlags.InvokeMethod, null, shell,new object[] { shortcutPath });
t.InvokeMember("TargetPath",System.Reflection.BindingFlags.SetProperty, null, shortcut,new object[] { thisAppPath });
t.InvokeMember("IconLocation",System.Reflection.BindingFlags.SetProperty, null, shortcut,new object[] { thisAppPath + ",0" });
t.InvokeMember("Save",System.Reflection.BindingFlags.InvokeMethod,null, shortcut, null);
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(shortcut);
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(shell);
//フォルダを開けて見せてあげる
System.Diagnostics.Process.Start("EXPLORER.EXE", "/select,\"" + shortcutPath + "\"");
MessageBox.Show("set start-up.", "BootRecorder:confirm", MessageBoxButton.OK, MessageBoxImage.Information);
}
private void btn_RemoveStartup_Click(object sender, RoutedEventArgs e)
{
//起動開始解除。自身のショートカットをスタートアップフォルダから削除
var shortcutPath = Environment.GetFolderPath(Environment.SpecialFolder.StartMenu) + "\\Programs\\Startup\\Bootrecorder.lnk";
if(File.Exists(shortcutPath) == true)
{
File.Delete(shortcutPath);
//フォルダを開けて見せてあげる
var StartupFolder = Environment.GetFolderPath(Environment.SpecialFolder.StartMenu) + "\\Programs\\Startup\\";
System.Diagnostics.Process.Start("EXPLORER.EXE", StartupFolder);
MessageBox.Show("Remove start-up.", "BootRecorder:confirm", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
private void btn_SetLocateLog_Click(object sender, RoutedEventArgs e)
{
//ログファイルの個別指定
var dialog = new OpenFileDialog();
dialog.Title = "Select Log File";
dialog.Filter = "Log files (*.log)|*.log|All files (*.*)|*.*";
if (File.Exists(G.Pref.LogPath) == true) dialog.FileName = System.IO.Path.GetFileName(G.Pref.LogPath);
{
dialog.InitialDirectory = System.IO.Path.GetDirectoryName(G.Pref.LogPath);
dialog.FileName = System.IO.Path.GetFileName(G.Pref.LogPath);
}
dialog.CheckFileExists = false;
if (dialog.ShowDialog() == true) G.Pref.LogPath = dialog.FileName;
else return;
G.Pref.Save(this);
ReadLog();
}
#三角形のボタン作成
せっかく作った画面右上・右下それぞれの隅っこへの飛ばし機能ですが、IF的にどう組み込んでいいのかなかなかいい案が思いつかなかったので、Windowの右上、右下の縁に三角形のボタンとして入れることにしました。また画面の最小化(というかHide)のボタンも欲しかったので右上への飛ばしボタンの下の三角をNotifizeボタンとして入れてあります。
ボタンの体裁定義はXAML側ResourceDictionary内で行ってます。ポリゴンの座標を記述。Polygon x:Name="ButtonTriangle" Points="0,0 16,16 16,0 0,0" のところですね。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:FileTestApp">
<Style x:Key="TriBtn1" TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Grid>
<Polygon x:Name="ButtonTriangle" Points="0,0 16,16 16,0 0,0"
Stroke="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}" StrokeThickness="0">
<Polygon.Fill>
<SolidColorBrush Color="{DynamicResource {x:Static SystemColors.ScrollBarColorKey}}"/>
</Polygon.Fill>
</Polygon>
<ContentPresenter Margin="0,6,0,0" TextBlock.FontSize="12"
TextBlock.TextAlignment="Center">
</ContentPresenter>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Path.Fill" Value="{DynamicResource {x:Static SystemColors.ActiveCaptionBrushKey}}" TargetName="ButtonTriangle" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
以下略
で本体側のXAMLでこのようにStyleを呼び出し。
<Button x:Name="BtnGoRT" Style="{StaticResource TriBtn1}" Content="" Height="16" Margin="174,0,0,0" VerticalAlignment="Top" FontSize="9" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" Foreground="{DynamicResource {x:Static SystemColors.WindowBrushKey}}" BorderBrush="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" Width="16" Click="BtnGoRT_Click" BorderThickness="0"/>
<Button x:Name="Btn_notifize" Style="{StaticResource TriBtn3}" Content="" HorizontalAlignment="Right" Height="16" VerticalAlignment="Top" Width="16" BorderBrush="{x:Null}" Click="Btn_notifize_Click" Background="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}" BorderThickness="0"/>
#親側からの子Windowの操作、子側からの別のクラスモジュール呼び出し
時報の画面は別Windowクラスで作成。また時報画面で表示するアナログ時計は、メインウインドウ側でアナログ時計を描画するルーチンを使いまわしてます。
まずは親からの時報画面の呼び出し。
private void MakeSignalWin()
{
//時報画面の作成準備
var SigWin = new signal();
SigWin.Opacity = 0;
SigWin.Owner = this;
SigWin.Show();
var hwnd = new WindowInteropHelper(SigWin).Handle;
Rect trg = new Rect(G.SW - SigWin.Width * G.SR, G.SH - SigWin.Height * G.SR, SigWin.Width * G.SR, SigWin.Height * G.SR);
NativeMethods.SetWindowPos(hwnd, IntPtr.Zero, (int)Math.Round(trg.X), (int)Math.Round(trg.Top),
(int)Math.Round(trg.Width), (int)Math.Round(trg.Height), Consts.SWP_NOZORDER);
SigWin.SignalStart();
}
時報画面となるsignalクラスのウインドウを作成してshowしてます。毎回newで作るべきか、最初だけnewで作って、以降は持ちまわるべきか少し迷いましたが、シンプルに毎回作成して毎回クローズする手順を取りました。ちなみにSetWindowPosはshowした後でないと効果がない。全部準備できたらSignalStartで子側のタイマーを起動させてます(newしたsigwin側でSignalStartを受け取る準備が完了しているか確証がないので、正確性に欠けてます)。
時報WINDOW側の記述は
private void SignalLoop(object sender, EventArgs e)
{
//時計描画
DateTime dt = DateTime.Now;
if (dt.Second != S.sv_sec)
{
DrawProc.DrawClock(sigCanvas, 3);
hhmm.Text = dt.ToString("HH:mm");
S.sv_sec = dt.Second;
}
//fade in /out
//中略
//子供側でfade in/out して一定時間過ぎたら自身をcloseする
}
#やってみての感想
自分自身これまでプロセスをベタで記述する手法に慣れてしまっていることもありWPFらしさが全くない単なるVB→C#コンバージョンになってしまった気がします。
WPFは特にクラスの最低限の知識がないと難しいという印象。XAML・データのBindingあたりがハードルが高い。逆に全体構造を正確に理解できていればFormsで実現が難しかったことが高い自由度でスマートに実現できるんだろーなという予想。