4
6

More than 3 years have passed since last update.

いまさらながらのアナログ時計作成(Visual Studio 2019編)

Posted at

1、概要

 いまさらながらですが、Visual Studio 2019でC#を使用したアナログ時計を作成してみました。Webを「C# アナログ時計の作り方」で検索すると多数の記事がヒットしますが、その多くが時計の針を線で描画しています。ここでは、目先を変えて針の画像を回転させて時を刻みたいと思います。画像を使用することで文字盤のみならず短針・長針・秒針までも好みのデザインにすることが可能となります。
ここでは、基本的なアナログ時計と色々な機能を追加したアナログ時計を作成します。
基本的なアナログ時計の表示例:
012.png
色々な機能を追加したアナログ時計の表示例:
026.png

2、ファイル構成

 下記に示すファイルで「基本的なアナログ時計」については、C#プログラムの部分のみをテキストファイルで、「機能を追加したアナログ時計」は、全てのファイルを次のGitHubからダウンロードできます。なお、ダウンロードしたZIPファイルを解凍し「Analog_Clock.sln」をダブルクリックすると、Visual Studio 2019が立ち上がりますので、修正あるいは実行ファイルを作成することができます。
- GitHub: Analog Clock 記事
- GitHub: Analog Clock ダウンロード

【ダウンロード対象のファイルの構成】

「.vs」:自動で作成されるフォルダ(隠しフォルダ:プロジェクトに関する情報、ビルドに関する情報が格納される)
「bin」:自動で作成されるフォルダ(コンパイル時に実行ファイルが格納される)
「obj」:自動で作成されるフォルダ(コンパイルに必要なファイルが生成される)
「Properties」:自動で作成されるフォルダ(コンパイルに必要なファイルが生成される)
「Resources」:リソース管理を選択した場合に自動で作成されるフォルダ(ここにプログラムで使用するリソースが格納される)
「Analog_Clock.csproj」:自動で作成されるファイル(プロジェクトの設定用ファイル)
「Analog_Clock.sln」:自動で作成されるファイル(ソリューション・ファイル:ソリューション全体を管理するための情報が格納されている)
「App.config」:自動で作成されるファイル(アプリケーション設定ファイル)
「Form1.cs」:自動で作成されるファイル(フォームの挙動を記述するC#ファイル:ユーザが詳細を記述する)
「Form1.Designer.cs」:自動で作成されるファイル(フォームの外観等が自動で記述される)
「Form1.resx」:自動で作成されるファイル(XMLリソース・ファイル)
「Form1_Basic.txt」:「基本的なアナログ時計」のC#プログラムファイル(今回、テキスト版として格納した)
「Program.cs」:自動で作成されるファイル(メイン関数、Form1.csを実行するための記述がある)

3、基本的なアナログ時計の作成

 初めに単純なアナログ時計を作成してみます。

Step-1: 準備

 前段階として次の画像を準備してください。なお、GitHubからダウンロードしたファイルを解凍すると「Resources」フォルダ内に当該ファイルが格納されています。
(a) 時計の文字盤画像
(b) 短針画像
(c) 長針画像
(d) 秒針画像

ご自身で画像を準備される場合には、次の点に気を付けて作成してください。
(i) 画像の縦横のピクセルサイズを同一にする。
(ii) 短針、長針、秒針の画像は中心から真上(12時方向)に描画する。
(iii) 透過PNGあるいは透過GIFで作成する。(基本的なアナログ時計の作成のみの場合には透過でなくても良いです。)

Visual Studioをインストールしていない方は、『Visual Studioダウンロード・サイト』から無料版で良いので入手してください。

Step-2: 新しいプロジェクトの作成

 新しいプロジェクトを作成し、時計を表示させるフォームを表示させます。
(2-1) Visual Studio 2019を起動すると次のような画面が表示されますので、「新しいプロジェクトの作成(N)」を選択してください。
001.png
(2-2) 使用するプログラム言語をC#、「Windowsフォームアプリケーション(New Framework)」を選択して「次へ(N)」をクリックしてください。
002.png
(2-3) プロジェクト名を入力し、「作成(C)」をクリックしてください。なお、「ソリューションとプロジェクトを同じディレクトリに配置する(D)」については、好みでチェックしてください。
003.png
(2-4) 以上で次の作成画面(Form1)が表示されます。
004.png

Step-3: 画面構成とリソースの設定

 時計の部品を配置し、対応するリソース(文字盤、針)を登録します。
(3-1) 「表示」から「ツールボックス」を選択します。
005.png
(3-2) 「ツールボックス」で「PictureBox」を4回「Form1」内にドラッグします。(文字盤、短針、長針、秒針の4つをそれぞれのPictureBoxに対応させる予定です)
006.png
(3-3) 「Form1」の空白の部分をダブルクリックしてください。
次の画面が表示され、プログラムを記述する準備ができました。
007.png
(3-4) ここで時計の部品となる画像をリソースに登録します。「プロジェクト」から「Analog_Clockのプロパティ(P)」を選択してください。
008.png
(3-5) 次に「プロパティ」の「リソース」を選択してください。
009.png
(3-6) Step-1で準備した文字盤、短針、長針、秒針の画像を「リソース」画面内にドラッグしてください。
010.png

Step-4: プログラムの記述

 上記(3-6)項で、「Form1.cs」をクリックすると(3-3)項の画面に戻ります。次のプログラムを入力(コピペ)してください。なお、ここで使用した画像の回転については、『NonSoft - Bitmapを中心点と角度を指定して回転するサンプル(C#.NET)』を参考にさせて戴きました。

Analog_Clock_C#_Source
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using System.Timers;

namespace Analog_Clock
{
    public partial class Form1 : Form
    {
        Bitmap Clock_Face = Properties.Resources.Clock_Face_006;        // 時計の文字盤
        Bitmap Clock_Second = Properties.Resources.Clock_Hand_001s;     // 秒針
        Bitmap Clock_Minute = Properties.Resources.Clock_Hand_001m;     // 長針
        Bitmap Clock_Hour = Properties.Resources.Clock_Hand_001h;       // 短針

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            System.Drawing.Graphics g;

            pictureBox1.Dock = DockStyle.Fill;                          // pictureBoxをForm1に合わせて適切なサイズに調節
            pictureBox1.SizeMode = PictureBoxSizeMode.Zoom;             // pictureBoxをサイズ比率を維持して拡大・縮小

            pictureBox2.Dock = DockStyle.Fill;
            pictureBox2.SizeMode = PictureBoxSizeMode.Zoom;

            pictureBox3.Dock = DockStyle.Fill;
            pictureBox3.SizeMode = PictureBoxSizeMode.Zoom;

            pictureBox4.Dock = DockStyle.Fill;
            pictureBox4.SizeMode = PictureBoxSizeMode.Zoom;

            Clock_Face.MakeTransparent();
            Clock_Hour.MakeTransparent();
            Clock_Minute.MakeTransparent();
            Clock_Second.MakeTransparent();

            g = pictureBox2.CreateGraphics();
            g.DrawImage(Clock_Hour, new System.Drawing.Point(0, 0));
            g.Dispose();

            g = pictureBox3.CreateGraphics();
            g.DrawImage(Clock_Minute, new System.Drawing.Point(0, 0));
            g.Dispose();

            g = pictureBox4.CreateGraphics();
            g.DrawImage(Clock_Second, new System.Drawing.Point(0, 0));
            g.Dispose();

            pictureBox1.Image = Clock_Face;

            pictureBox2.Parent = pictureBox1;
            pictureBox3.Parent = pictureBox2;
            pictureBox4.Parent = pictureBox3;

            pictureBox2.BackColor = Color.Transparent;              // 画像を重ねるため、背景色を透過にする
            pictureBox3.BackColor = Color.Transparent;
            pictureBox4.BackColor = Color.Transparent;

            System.Timers.Timer timer = new System.Timers.Timer(1000);
            timer.Elapsed += (sender_Temp, e_Temp) =>
            {
                Draw_Clock();
            };
            timer.Start();
        }

        // 時計の表示
        private void Draw_Clock()
        {
            DateTime time = DateTime.Now;
            float SecondAng = (float)(time.Second * 6.0);
            float MinuteAng = (float)((time.Minute + time.Second / 60.0) * 6.0);
            float HourAng = (float)((time.Hour + time.Minute / 60.0) * 30.0);

            pictureBox4.Image = RotateBitmap(Clock_Second, SecondAng, Clock_Second.Width / 2, Clock_Second.Height / 2);
            pictureBox3.Image = RotateBitmap(Clock_Minute, MinuteAng, Clock_Minute.Width / 2, Clock_Minute.Height / 2);
            pictureBox2.Image = RotateBitmap(Clock_Hour, HourAng, Clock_Hour.Width / 2, Clock_Hour.Height / 2);
        }

        // 時計の針画像の回転
        public Bitmap RotateBitmap(Bitmap org_bmp, float angle, int x, int y)
        {
            Bitmap result_bmp = new Bitmap((int)org_bmp.Width, (int)org_bmp.Height);
            Graphics g = Graphics.FromImage(result_bmp);

            g.TranslateTransform(-x, -y);
            g.RotateTransform(angle, System.Drawing.Drawing2D.MatrixOrder.Append);
            g.TranslateTransform(x, y, System.Drawing.Drawing2D.MatrixOrder.Append);
            g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBilinear;
            g.DrawImageUnscaled(org_bmp, 0, 0);
            g.Dispose();

            return result_bmp;
        }
    }
}

Step-5: デバック及びビルド

 (5-1) 画面下部で「問題は見つかりませんでした」の表示を確認後、「Debug」設定で「開始」をクリックするとデバッグモードでビルドできます。
011.png
(5-2) 次の画面のようにアナログ時計が表示されると思います。終了は上部にある赤い「■」です。
012.png

4、 色々な機能を追加してみる

 次に幾つかの機能を追加してみます。追加する項目は次の通りです
(a) メニューを追加
(b) プログラムの終了設定
(c) 文字盤と針を選択可能とする
(d) 時計の表示位置、サイズを変更可能とする
(e) 時報、アラームを設定可能とする
(f) アラームOFF
(g) 各設定の保存と読み込み
ここで使用した時報ならびにアラーム音は、フリー素材サイトOtoLogic(オトロジック)様からダウンロードしたMP3ファイルをwaveに変換したものです。なお、GitHubからダウンロードしたZIPファイルを解凍し「Analog_Clock.sln」をダブルクリックすると、Visual Studio 2019が立ち上がり実行ファイルを作成することができます。
色々な機能を追加したアナログ時計の表示例を次に示します。
026.png

Step-1:メニューを追加

 最終的に時計のみを表示したいため、Windowの枠ならびにタイトルバーを表示しないようにします。このため、タスクバーに表示したアイコンからメニューを表示し各種設定を行いたいと思います。メニューの表示項目は、次の通りです。
(i) 時計表示の終了
(ii) 文字盤・針の画像変更
(iii) 時計の位置、サイズ変更
(iv) 時報、アラームの設定
(v) アラームOFF

(1-1)タスクバーにアイコンを表示させるには、「NotifyIcon」を使用します。
「Form1.cs[デザイン]」を選択し、「ツールボックス」から「NotifyIcon」を「Form1」の空白部分にドラッグします。
020.png
さらに、「ContextMenuStrip」も同様にドラッグします。
021.png
(1-2)「NotifyIcon」と「ContextMenuStrip」は、「Form1」の下側に表示されます。「NotifyIcon」と「ContextMenuStrip」を関連付けるため、「NotifyIcon1」を選択し、プロパティの「ContextMenuStrip」の項目で「ContextMenuStrip1」を設定します。
022.png
(1-3) 「ContextMenuStrip1」を選択し、上部に「ここに入力」と表示されている箇所に次のようにメニュー項目を入力します。
023.png
(1-4) Form1の背景画像の透過
「Form1.cs[デザイン]」を選択し、プロパティの「TransparencyKey」で「Control」を選択することでForm1の背景画像の透過することができます。なお、「Control」は、カラー選択の「システム」から指定できます。
027.png

Step-2:時計表示の終了

 「ContextMenuStrip1」で入力した「Exit」をダブルクリックすると「Form1.cs」内に次のプログラムが表示されます。

        private void exitToolStripMenuItem_Click(object sender, EventArgs e)
        {

        }

表示されたプログラム内に次のようにプログラムの終了処理を記述します。

        // Context MenuでExitを選択した場合の終了処理
        private void exitToolStripMenuItem_Click(object sender, EventArgs e)
        {
            pictureBox1.Dispose();
            pictureBox2.Dispose();
            pictureBox3.Dispose();
            pictureBox4.Dispose();
            Application.Exit();
        }

Step-3:文字盤と針の選択

 上記と同様に「ContextMenuStrip1」で入力した「Clock_Face」をダブルクリックし、次のように入力してダイアログボックスを表示させます。

        // Context MenuでClock_Faceを選択した場合のダイアログボックス表示
        private void clockFaceToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Dialog_Display dialog1 = new Dialog_Display();
            dialog1.Show();
        }

ここで表示するダイアログボックスの内容は次の通りです。ここでは文字盤と針の選択をリストボックスで、不透明度をトラックバーで設定するようにしました。

   // Display用のダイアログボックス
    public class Dialog_Display : Form
    {
        Label label_Face;
        Label label_Hands;
        Label label_Opacity;
        Label label_OpacityInfo;
        ListBox listBox_Face;
        ListBox listBox_Hands;
        public TextBox textBox_Face;
        public TextBox textBox_Hands;
        public TextBox textBox_Opacity;
        public TrackBar trackbar_Opacity;
        public string Selected_Face;
        public string Selected_Hands;
        Button Close_button;

        public Dialog_Display()
        {
            // ダイアログボックスの設定
            this.MaximizeBox = false;                                   // 最大化ボタン
            this.MinimizeBox = false;                                   // 最小化ボタン
            this.ShowInTaskbar = true;                                  // タスクバー上に表示
            this.FormBorderStyle = FormBorderStyle.FixedDialog;         // 境界のスタイル
            this.StartPosition = FormStartPosition.Manual;              // 任意の位置に表示
            this.Size = new Size(300, 400);                             // ダイアログボックスのサイズ指定
            this.Location = new Point(100, 100);                        // ダイアログボックスの表示位置
            this.Text = "Set the Analog Clock";                         // タイトル

            label_Face = new Label()
            {
                Text = "時計の文字盤選択:",
                Location = new Point(10, 10),
            };
            label_Face.Size = new Size(120, 25);
            listBox_Face = new ListBox()
            {
                Location = new Point(50, 35),
            };
            listBox_Face.Size = new Size(200, 60);
            for (int i = 0; i < Form1.ClockFaceData.Length; i++)
            {
                listBox_Face.Items.Add(Form1.ClockFaceData[i]);
            }
            listBox_Face.SelectedIndexChanged += ListBox_Face_SelectedIndexChanged;
            Selected_Face = Form1.ClockFaceData[Form1.Face_No];
            textBox_Face = new TextBox()
            {
                Text = Selected_Face,
                Location = new Point(130, 5),
                ReadOnly = true,
            };
            textBox_Face.Size = new Size(150, 25);
            label_Hands = new Label()
            {
                Text = "時計の針選択:",
                Location = new Point(10, 100),
            };
            label_Hands.Size = new Size(100, 25);
            listBox_Hands = new ListBox()
            {
                Location = new Point(50, 125),
            };
            listBox_Hands.Size = new Size(180, 60);
            for (int i = 0; i < Form1.ClockHandsData.Length; i++)
            {
                listBox_Hands.Items.Add(Form1.ClockHandsData[i]);
            }
            listBox_Hands.SelectedIndexChanged += ListBox_Hands_SelectedIndexChanged;
            Selected_Hands = Form1.ClockHandsData[Form1.Hands_No];
            textBox_Hands = new TextBox()
            {
                Text = Selected_Hands,
                Location = new Point(120, 95),
                ReadOnly = true,
            };
            textBox_Hands.Size = new Size(160, 25);

            label_Opacity = new Label()
            {
                Text = "時計の不透明度(%):",
                Location = new Point(10, 210),
            };
            label_Opacity.Size = new Size(120, 25);
            label_OpacityInfo = new Label()
            {
                Text = "不透明度は、本ソフト再立ち上げ後有効になります。",
                Location = new Point(10, 235),
            };
            label_OpacityInfo.Size = new Size(280, 25);
            textBox_Opacity = new TextBox()
            {
                Text = (Form1.Opacity_Data * 100).ToString(),
                Location = new Point(150, 205),
                TextAlign = HorizontalAlignment.Center,
                ReadOnly = true,
            };
            textBox_Opacity.Size = new Size(50, 25);
            trackbar_Opacity = new TrackBar()
            {
                Location = new Point(20, 260),
                Minimum = 0,
                Maximum = 100,
                Value = (int)(Form1.Opacity_Data * 100),
                TickFrequency = 10,
                SmallChange = 1,
                LargeChange = 10,
                AutoSize = false,
                Size = new Size(240, 40),
            };
            trackbar_Opacity.ValueChanged += Trackbar_Opacity_ValueChanged;

            Close_button = new Button()
            {
                Text = "Close",
                Location = new Point(100, 320),
            };
            Close_button.Click += new EventHandler(Close_button_Clicked);

            this.Controls.AddRange(new Control[]
            {
                label_Face, label_Hands, label_Opacity, label_OpacityInfo,
                listBox_Face, listBox_Hands,
                textBox_Opacity, textBox_Face, textBox_Hands,
                trackbar_Opacity,
                Close_button
            });
        }

        // 時計の文字盤を変更した時の処理
        void ListBox_Face_SelectedIndexChanged(object sender, EventArgs e)
        {
            Form1.Face_No = listBox_Face.SelectedIndex;
            Form1.Face_flag = 1;
            textBox_Face.Text = Form1.ClockFaceData[Form1.Face_No];
        }

        // 時計の針を変更した時の処理
        void ListBox_Hands_SelectedIndexChanged(object sender, EventArgs e)
        {
            Form1.Hands_No = listBox_Hands.SelectedIndex;
            Form1.Hands_flag = 1;
            textBox_Hands.Text = Form1.ClockHandsData[Form1.Hands_No];
        }

        // 不透明度の値が変更された時の処理
        void Trackbar_Opacity_ValueChanged(object s, EventArgs e)
        {
            textBox_Opacity.Text = trackbar_Opacity.Value.ToString();
            Form1.Opacity_Data = trackbar_Opacity.Value / 100.0;
        }

        // クローズボタンが押された時の処理
        void Close_button_Clicked(object sender, EventArgs e)
        {
            CSV_Save CSV_Write = new CSV_Save();
            CSV_Write.Save_CSV();
            this.Close();
        }
    }

Step-4:時計の表示位置、サイズの変更

 上記Step-3と同様にダイアログボックスを表示させます。時計の表示位置設定とサイズ設定はトラックバーを使用しました。

Step-5:時報、アラーム設定

 これも上記Step-3と同様にダイアログボックスを表示させます。時報とアラームのON/OFFは、フリップフロップボタンとし、アラームの音色はリストボックス、アラーム時間の設定はトラックバーを使用しました。

Step-6:アラームOFF

 アラームOFFも実装しました。内容は次の通りです。

        // Context MenuでAlarm_OFFを選択した場合の処理
        private void alarmOFFToolStripMenuItem_Click(object sender, EventArgs e)
        {
            player_T_Signal.Stop();
            player_Alarm.Stop();
        }

Step-7:各設定の保存と読み込み

 本プログラム開始時に設定を読み込むため、Form1に次のプログラムを追記しました。

            CSV_Read CSV_Load = new CSV_Read();
            try
            {
                CSV_Load.Read_CSV();
            }
            catch (Exception)
            {
                CSV_Save CSV_Write = new CSV_Save();
                CSV_Write.Save_CSV();
            }

本プログラムでは、各種設定を以下のようにCSVファイルで保存・読み込むようにしました。

    public class CSV_Read      // CSV Read
    {
        string[] Temp_Data = new string[10];
        public void Read_CSV()
        {
            StreamReader sr = new StreamReader(@"AnalogClock_Setting.csv", Encoding.GetEncoding("Shift_JIS"));
            try
            {
                while (sr.EndOfStream == false)
                {
                    string line = sr.ReadLine();
                    string[] values = line.Split(',');
                    for (int i = 0; i < values.Length; i++)
                    {
                        Temp_Data[i] = values[i];
                    }
                    if (Temp_Data[0] == "Face_No")
                    {
                        Form1.Face_No = int.Parse(Temp_Data[1]);
                    }
                    if (Temp_Data[0] == "Face_flag")
                    {
                        Form1.Face_flag = int.Parse(Temp_Data[1]);
                    }
                    if (Temp_Data[0] == "Hands_No")
                    {
                        Form1.Hands_No = int.Parse(Temp_Data[1]);
                    }
                    if (Temp_Data[0] == "Hands_flag")
                    {
                        Form1.Hands_flag = int.Parse(Temp_Data[1]);
                    }
                    if (Temp_Data[0] == "Size_Data")
                    {
                        Form1.Size_Data = int.Parse(Temp_Data[1]);
                    }
                    if (Temp_Data[0] == "Pos_X")
                    {
                        Form1.Pos_X = int.Parse(Temp_Data[1]);
                    }
                    if (Temp_Data[0] == "Pos_Y")
                    {
                        Form1.Pos_Y = int.Parse(Temp_Data[1]);
                    }
                    if (Temp_Data[0] == "T_Signal_flag")
                    {
                        Form1.T_Signal_flag = int.Parse(Temp_Data[1]);
                    }
                    if (Temp_Data[0] == "Alarm_flag")
                    {
                        Form1.Alarm_flag = int.Parse(Temp_Data[1]);
                    }
                    if (Temp_Data[0] == "Alarm_No")
                    {
                        Form1.Alarm_No = int.Parse(Temp_Data[1]);
                    }
                    if (Temp_Data[0] == "Alarm_H")
                    {
                        Form1.Alarm_H = int.Parse(Temp_Data[1]);
                    }
                    if (Temp_Data[0] == "Alarm_M")
                    {
                        Form1.Alarm_M = int.Parse(Temp_Data[1]);
                    }
                    if (Temp_Data[0] == "Opacity_Data")
                    {
                        if (Temp_Data[1] == null)
                        {
                            Form1.Opacity_Data = 0.0;
                        }
                        else
                        {
                            Form1.Opacity_Data = double.Parse(Temp_Data[1]);
                        }
                    }
                }
            }
            finally
            {
                sr.Close();
            }
        }
    }

    public class CSV_Save      // CSV Save
    {
        public void Save_CSV()
        {
            try
            {
                Encoding sjisEnc = Encoding.GetEncoding("Shift_JIS");
                StreamWriter file = new StreamWriter(@"AnalogClock_Setting.csv", false, sjisEnc);
                file.WriteLine("Face_No" + "," + Form1.Face_No.ToString());
                file.WriteLine("Face_flag" + "," + "1");
                file.WriteLine("Hands_No" + "," + Form1.Hands_No.ToString());
                file.WriteLine("Hands_flag" + "," + "1");
                file.WriteLine("Size_Data" + "," + Form1.Size_Data.ToString());
                file.WriteLine("Pos_X" + "," + Form1.Pos_X.ToString());
                file.WriteLine("Pos_Y" + "," + Form1.Pos_Y.ToString());
                file.WriteLine("T_Signal_flag" + "," + Form1.T_Signal_flag.ToString());
                file.WriteLine("Alarm_flag" + "," + Form1.Alarm_flag.ToString());
                file.WriteLine("Alarm_No" + "," + Form1.Alarm_No.ToString());
                file.WriteLine("Alarm_H" + "," + Form1.Alarm_H.ToString());
                file.WriteLine("Alarm_M" + "," + Form1.Alarm_M.ToString());
                file.WriteLine("Opacity_Data" + "," + Form1.Opacity_Data.ToString("0.00"));

                file.Close();
            }
            catch (Exception e)
            {
                MessageBox.Show("Save Error: " + e.Message);
            }
        }
    }

5、Reference

-Visual Studio 2019:ダウンロード・ページ
-NonSoft - Bitmapを中心点と角度を指定して回転するサンプル(C#.NET)
-フリー素材サイトOtoLogic(オトロジック)

4
6
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
6