ふとデスクトップに現在時刻を描画する自作アプリをつくってみたいと思い、やってみたのですが、意外に苦戦してしまいました。
Contents
残念ですが失敗です
最初に思いついた方法はディスプレイのデバイスコンテキストのハンドルを取得し、そこに現在時刻を描画するという方法です。
しかしこの方法では前回描画された文字のうえにさらに描画がおこなわれてしまうので文字が重なってしまいます。前回描画された文字列を消してから描画しなければなりません。DllImport 属性をもちいてdll関数の呼び出しをするという面倒なことをしたのですが、残念な結果となりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
using System; using System.Drawing; using System.Runtime.InteropServices; public partial class Form1 : Form { [DllImport("User32.dll")] static extern IntPtr GetDC(IntPtr hwnd); [DllImport("User32.dll")] static extern void ReleaseDC(IntPtr hwnd, IntPtr dc); Timer Timer = new Timer(); public Form1() { InitializeComponent(); Timer.Interval = 1000; Timer.Tick += Timer_Tick; Timer.Start(); } private void Timer_Tick(object sender, EventArgs e) { // 現在時刻から描画する文字列を取得する DateTime now = DateTime.Now; string nowText = String.Format("{0:00}:{1:00}:{2:00}", now.Hour, now.Minute, now.Second); Font timeFont = new Font("Arial", 80); // ディスプレイデバイスコンテキストのハンドルを取得し、そこに文字列を描画する IntPtr desktopDC = GetDC(IntPtr.Zero); using (Graphics g = Graphics.FromHdc(desktopDC)) { g.DrawString(nowText, timeFont, Brushes.Red, new Point(30, 30)); } timeFont.Dispose(); ReleaseDC(IntPtr.Zero, desktopDC); } } |
背景が透明なフォームに文字を表示させる
ではこの方法はどうでしょうか? 以前、フォームに穴をあけて画面録画できるようにするでフォームに穴をあける方法を紹介しました。これを使うと以下のようなコードになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
using System; using System.Drawing; public partial class Form1 : Form { Timer Timer = new Timer(); public Form1() { InitializeComponent(); Timer.Interval = 1000; Timer.Tick += Timer_Tick; Timer.Start(); this.BackColor = Color.Magenta; this.TransparencyKey = Color.Magenta; } string NowText = ""; private void Timer_Tick(object sender, EventArgs e) { DateTime now = DateTime.Now; NowText = String.Format("{0:00}:{1:00}:{2:00}", now.Hour, now.Minute, now.Second); Invalidate(); } protected override void OnPaint(PaintEventArgs e) { Font timeFont = new Font("Arial", 80); e.Graphics.DrawString(NowText, timeFont, Brushes.Red, new Point(30, 30)); timeFont.Dispose(); base.OnPaint(e); } } |
タイトルバーの非表示と終了させるための別の手段
これだと一応、クライアント領域は透明になり、うまくいきます。ただタイトルバーやウィンドウの境界線が透明にならないのでもうすこし工夫します。
タイトルバーが透明になってしまうと移動させたり終了させることができなくなってしまいます。そこで別の方法でこれらができるようにしておかなければなりません。
それからこのままではタスクバーで[デスクトップを表示]を選択すると、このフォーム自体も最小化されてしまい表示が消えてしまいます。これでは困るのでthis.MinimizeBox = false; として他のウィンドウをすべて最小化したときに見えなくなってしまうことを防いでいます。
以下のコードではthis.FormBorderStyle = FormBorderStyle.Noneとすることでタイトルバーと境界線を非表示にします。そして通知領域にアイコンを表示させ、これをクリックすると終了させるためのメニューが表示されます。そして終了を選択するとアプリケーションが終了します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
using System; using System.Drawing; using System.Windows.Forms; public partial class Form1 : Form { Timer Timer = new Timer(); NotifyIcon NotifyIcon = new NotifyIcon(); public Form1() { InitializeComponent(); Timer.Interval = 1000; Timer.Tick += Timer_Tick; Timer.Start(); // 背景色をMagentaにしてMagentaの部分は透明に(全体が透明になる) this.BackColor = Color.Magenta; this.TransparencyKey = Color.Magenta; this.FormBorderStyle = FormBorderStyle.None; // 他のウィンドウの下になると見えなくなるので常に最前面に表示させる this.TopMost = true; NotifyIcon.Icon = Properties.Resources.Icon1; NotifyIcon.MouseClick += NotifyIcon_MouseClick; NotifyIcon.Visible = true; InitNotifyIconMenu(); this.MinimizeBox = false; this.ShowInTaskbar = false; // タスクバーのなかにも表示させない } // 通知領域のアイコンをクリックしたときに表示されるメニューを初期化する void InitNotifyIconMenu() { MenuItem[] menuItem = { new MenuItem("終了", (sender1, e1) => { NotifyIcon.Visible = false; Application.Exit(); }), }; NotifyIcon.ContextMenu = new ContextMenu(menuItem); } // 通知領域のアイコンをクリックしたらメニューを表示させる private void NotifyIcon_MouseClick(object sender, MouseEventArgs e) { NotifyIcon.ContextMenu.Show(this, PointToClient(Control.MousePosition)); } string NowText = ""; private void Timer_Tick(object sender, EventArgs e) { DateTime now = DateTime.Now; NowText = String.Format("{0:00}:{1:00}:{2:00}", now.Hour, now.Minute, now.Second); Invalidate(); } protected override void OnPaint(PaintEventArgs e) { Font timeFont = new Font("Arial", 80); e.Graphics.DrawString(NowText, timeFont, Brushes.Red, new Point(0, 0)); timeFont.Dispose(); base.OnPaint(e); } } |
表示色や表示場所の変更
一応、これで表示はされるのですが、表示色や表示場所を選択したい、他のウィンドウがあるときはそのウィンドウを隠さないようにしたいなどの設定もできるようにします。
フォームの場所を変更するためには透明でない部分をドラッグすればよいのですが、それだと使いにくいので一時的に背景を白で描画するようにします。ドラッグでフォームを移動させるための処理は後述します。
また文字色を変化させるときはColorDialogをつかって設定しますが、そのときにColor.Magentaが選択されてしまうと、文字まで透明になってしまいます。そこでその場合はColor.Magentaから少しズラした色を使うようにします。
フォームを最前面にするのであればthis.TopMost = true;とすればいいのですが、最後面にするにはどうすればいいのでしょうか? ここではWindows API関数のSetWindowPos関数を使います。第2引数にHWND_BOTTOMを使い、第3から第6引数まで無視するために第7引数にSWP_NOSIZE | SWP_NOMOVEを使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
public partial class Form1 : Form { Color TextColor = Color.Empty; void InitNotifyIconMenu() { MenuItem[] menuItem = { new MenuItem("終了", (sender1, e1) => { NotifyIcon.Visible = false; Application.Exit(); }), new MenuItem("背景を白で描画", (sender1, e1) => { this.BackColor = Color.White; }), new MenuItem("背景を透明に", (sender1, e1) => { this.BackColor = Color.Magenta; }), new MenuItem("文字色を変更する", (sender1, e1) => { ColorDialog dialog = new ColorDialog(); dialog.Color = TextColor == Color.Empty ? Color.Red : TextColor; if(dialog.ShowDialog() == DialogResult.OK) { TextColor = dialog.Color; // Color.Magentaが設定された場合、文字まで透明になってしまうので色をずらす if(TextColor.ToArgb() == Color.Magenta.ToArgb()) TextColor = Color.FromArgb(255, 255, 1, 255); } dialog.Dispose(); }), new MenuItem("フォームを最前面に", (sender1, e1) => { SetWindowPos(this.Handle, HWND_TOPMOST, 0, 0, 0, 0, TOPMOST_FLAGS); }), new MenuItem("フォームを最後面に", (sender1, e1) => { this.TopMost = false; SetWindowPos(this.Handle, HWND_BOTTOM, 0, 0, 0, 0, TOPMOST_FLAGS); }), }; NotifyIcon.ContextMenu = new ContextMenu(menuItem); } protected override void OnPaint(PaintEventArgs e) { Font timeFont = new Font("Arial", 80); SolidBrush brush; if (TextColor == Color.Empty) brush = new SolidBrush(Color.Red); else brush = new SolidBrush(TextColor); e.Graphics.DrawString(NowText, timeFont, brush, new Point(0, 0)); brush.Dispose(); timeFont.Dispose(); base.OnPaint(e); } [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint flags); const uint SWP_NOSIZE = 0x0001; const uint SWP_NOMOVE = 0x0002; static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); static readonly IntPtr HWND_BOTTOM = new IntPtr(1); const uint TOPMOST_FLAGS = (SWP_NOSIZE | SWP_NOMOVE); // SetWindowPos関数の引数 x, y, cx, cyを無視 } |
ドラッグアンドドロップでフォームを移動させる処理
ドラッグでフォームを移動する処理を示します。
ドラッグが開始されたらマウスキャプチャしてマウスポインタの座標を記憶します。そしてマウスキャプチャされているときにマウスが移動したら、移動分だけフォームの座標を移動させる処理を繰り返します。最後にマウスボタンが離されたらマウスキャプチャを解除します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public partial class Form1 : Form { int StartX = 0; int StartY = 0; protected override void OnMouseDown(MouseEventArgs e) { StartX = Control.MousePosition.X; StartY = Control.MousePosition.Y; this.Capture = true; } protected override void OnMouseMove(MouseEventArgs e) { if (this.Capture) { int dX = Control.MousePosition.X - StartX; int dY = Control.MousePosition.Y - StartY; StartX = Control.MousePosition.X; StartY = Control.MousePosition.Y; Point point = new Point(this.Location.X + dX, this.Location.Y + dY); this.Location = point; } } protected override void OnMouseUp(MouseEventArgs e) { this.Capture = false; } } |