はじめに
突然ですが、皆さん、勤務表って書かれたことはありませんか?私の所属会社はSES企業ということもあり、毎月の勤怠把握のためにExcelのテンプレートを利用した勤務表を作成、提出しています。
月~金の週5日の決まった時間でお仕事をしているのであればコピペやオートフィルを用いて簡単に作成することができると思うのですが、私が以前シフト勤務をしていた際、勤務日や時間が不規則だったため毎月勤務表の作成に時間がかかっていました。そこで、個人開発の一つのアイデアとしてGUIを用いて勤務表を編集するアプリケーションを作成したいと感じ、今回実際に作成を行うことにしました。
使用言語やツールの選定
今回実現したいことをまとめると、
①Excelの勤務表をGUIアプリケーションで操作する
②その際、可能な限りExcelの直接操作を減らせるようにする(アプリケーションのみで完結させたい)
③自分以外の環境でも使用できるようにする
④コードの冗長化を防ぐ
①はそもそもの前提として、②は最終的にExcelファイル自体を触るのであればわざわざアプリケーションで作成する意味がないため、③は前回がbotだったため考えていませんでしたが今回は異なる環境でも動くことを想定して作成しました。(後述します)
④は今まであまり意識することが出来ていなかったため意識する目標として記述しています。
前提知識で、Windowsにおける開発環境として.NET Frameworkが使用できるということは以前から知っており、Excelとの連携も行いやすいとのことだったので.NET Frameworkを使用することにしました。また、GUIを作成する必要があったのですが、.NETにはWPF(Windows Presentation Foundation) というGUI開発環境も搭載されていたためこちらを使用することにしました。
設計
アプリケーションを作成するにあたって、どういった機能が必要かを考えながら設計を行いました。
今回は必要な機能を考えた結果2画面構成で行うこととし、各画面でどういった動作をするか決めていきます。
文章だけではイメージが伝わりづらいと思いますので完成後の画面を用いて説明します。
1画面目
起動時はこの画面になります。
[取得]ボタンで指定したパス配下の.xlsxファイルを取得し、どのファイルを編集するか選択することが出来ます。選択せずに[次へ]ボタンを押した場合、
このようにエラーを吐くようにしてあります。
ファイルの選択はどうしても実装したかった機能の一つで、ファイルの編集を行う前にどのファイルを編集するかGUI上で選択することで編集ミスを防ぐ目的があります。実装の手間もそれほどかからず、見栄えもよいので気に入っています。
2画面目
この画面では、実際にExcelファイルの編集を行っていきます。
勤務開始時刻と終了時刻をプルダウンメニューから選択し、[登録]ボタンで書き込みを行います。他、編集を便利にする機能として、[現在時刻]ボタンを押すと現在の日時を勤務開始時刻に反映します。また、[copy]ボタンを押下すると勤務開始時刻を勤務終了時刻にコピーします。この二つの機能はアプリケーションを作成していて、年月欄を変更する機会が少ないと感じ実装しました(年月欄も必要な場面は0ではないので一応残しています)
そして、下部にある[読み込み]ボタンですが、別途に用意したテキストファイルを使用してシートの勤務時間以外の部分を記入するのに使用します。編集時のセル番地を求めるのも同様の方法を用いており、異なる環境でも動作させるための工夫にあたるのですが、詳しくは実際のスクリプトを見ながら解説します。
[終了]ボタンは、Excelファイルを終了するためのボタンになります。上部に注釈としていますが、×閉じしてしまうとファイルが開いたままになってしまうためきちんと終了処理を挟んであげる必要があります。
実際に作成したスクリプト
今回は試験的にgithubにもpushしてみました。
一応こちらからでもスクリプトをご覧いただけます。(git及びgithubの使用は初めてですが恐らくpushできているかと思われます)
XAML編
今回WPFを使うにあたってGUIの作成をXAMLという言語を用いて作成しました。言語といっても目を通してみると意外とわかりやすく、作成にはそれほど苦労しませんでした。
Page1
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Work_Scheduler"
x:Class="Work_Scheduler.Page1"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Title="Page1">
<Grid>
<TextBlock x:Name="Console" HorizontalAlignment="Center" VerticalAlignment="Top" FontWeight="Bold" FontSize="18" TextAlignment="Center" Margin="0,40,0,0">
取得ボタンでファイルを取得し、選択した状態で次へを押してください
</TextBlock>
<ListBox x:Name="FileList" HorizontalAlignment="Left" Height="200" Margin="40,120,0,0" VerticalAlignment="Top" Width="710"/>
<StackPanel Grid.Row="1" Orientation="Vertical" HorizontalAlignment="Center">
<Button x:Name="Get" Content="取得" Width="70" Margin="80" Click="Button_Click"/>
</StackPanel>
<StackPanel Grid.Row="2" Orientation="Vertical" VerticalAlignment="Bottom" HorizontalAlignment="Right">
<Button x:Name="Next" Content="次へ" Width="70" Margin="50" Click="Button_Click"/>
</StackPanel>
</Grid>
</Page>
Page2
<Page x:Class="Work_Scheduler.Page2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Work_Scheduler"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Title="Page2">
<Grid>
<TextBlock x:Name="Console2" HorizontalAlignment="Stretch" Height="38" Margin="10,26,10,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="600"/>
<ComboBox x:Name="Year" HorizontalAlignment="Left" Width="50" Height="20" VerticalAlignment="Top" Margin="200,100" IsEditable="True">
<ComboBoxItem Content="2022" />
<ComboBoxItem Content="2023" />
<ComboBoxItem Content="2024" />
<ComboBoxItem Content="2025" />
<ComboBoxItem Content="2026" />
</ComboBox>
<ComboBox x:Name="Month" HorizontalAlignment="Left" Width="50" Height="20" VerticalAlignment="Top" Margin="300,100" IsEditable="True">
<ComboBoxItem Content="1" />
<ComboBoxItem Content="2" />
<ComboBoxItem Content="3" />
<ComboBoxItem Content="4" />
<ComboBoxItem Content="5" />
<ComboBoxItem Content="6" />
<ComboBoxItem Content="7" />
<ComboBoxItem Content="8" />
<ComboBoxItem Content="9" />
<ComboBoxItem Content="10" />
<ComboBoxItem Content="11" />
<ComboBoxItem Content="12" />
</ComboBox>
<ComboBox x:Name="Day" HorizontalAlignment="Left" Width="50" Height="20" VerticalAlignment="Top" Margin="400,100,0,1" IsEditable="True">
<ComboBoxItem Content="1" />
<ComboBoxItem Content="2" />
<ComboBoxItem Content="3" />
<ComboBoxItem Content="4" />
<ComboBoxItem Content="5" />
<ComboBoxItem Content="6" />
<ComboBoxItem Content="7" />
<ComboBoxItem Content="8" />
<ComboBoxItem Content="9" />
<ComboBoxItem Content="10" />
<ComboBoxItem Content="11" />
<ComboBoxItem Content="12" />
<ComboBoxItem Content="13" />
<ComboBoxItem Content="14" />
<ComboBoxItem Content="15" />
<ComboBoxItem Content="16" />
<ComboBoxItem Content="17" />
<ComboBoxItem Content="18" />
<ComboBoxItem Content="19" />
<ComboBoxItem Content="20" />
<ComboBoxItem Content="21" />
<ComboBoxItem Content="22" />
<ComboBoxItem Content="23" />
<ComboBoxItem Content="24" />
<ComboBoxItem Content="25" />
<ComboBoxItem Content="26" />
<ComboBoxItem Content="27" />
<ComboBoxItem Content="28" />
<ComboBoxItem Content="29" />
<ComboBoxItem Content="30" />
<ComboBoxItem Content="31" />
</ComboBox>
<ComboBox x:Name="Hour" HorizontalAlignment="Left" Width="50" Height="20" VerticalAlignment="Top" Margin="500,100,0,1" SelectedIndex="-1" IsEditable="True" Visibility="Visible">
<ComboBoxItem Content="00" />
<ComboBoxItem Content="01" />
<ComboBoxItem Content="02" />
<ComboBoxItem Content="03" />
<ComboBoxItem Content="04" />
<ComboBoxItem Content="05" />
<ComboBoxItem Content="06" />
<ComboBoxItem Content="07" />
<ComboBoxItem Content="08" />
<ComboBoxItem Content="09" />
<ComboBoxItem Content="10" />
<ComboBoxItem Content="11" />
<ComboBoxItem Content="12" />
<ComboBoxItem Content="13" />
<ComboBoxItem Content="14" />
<ComboBoxItem Content="15" />
<ComboBoxItem Content="16" />
<ComboBoxItem Content="17" />
<ComboBoxItem Content="18" />
<ComboBoxItem Content="19" />
<ComboBoxItem Content="20" />
<ComboBoxItem Content="21" />
<ComboBoxItem Content="22" />
<ComboBoxItem Content="23" />
<ComboBoxItem Content="24" />
</ComboBox>
<ComboBox x:Name="Minute" HorizontalAlignment="Left" Width="50" Height="20" VerticalAlignment="Top" Margin="600,100,0,1" IsEditable="True">
<ComboBoxItem Content="00" />
<ComboBoxItem Content="01" />
<ComboBoxItem Content="02" />
<ComboBoxItem Content="03" />
<ComboBoxItem Content="04" />
<ComboBoxItem Content="05" />
<ComboBoxItem Content="06" />
<ComboBoxItem Content="07" />
<ComboBoxItem Content="08" />
<ComboBoxItem Content="09" />
<ComboBoxItem Content="10" />
<ComboBoxItem Content="11" />
<ComboBoxItem Content="12" />
<ComboBoxItem Content="13" />
<ComboBoxItem Content="14" />
<ComboBoxItem Content="15" />
<ComboBoxItem Content="16" />
<ComboBoxItem Content="17" />
<ComboBoxItem Content="18" />
<ComboBoxItem Content="19" />
<ComboBoxItem Content="20" />
<ComboBoxItem Content="21" />
<ComboBoxItem Content="22" />
<ComboBoxItem Content="23" />
<ComboBoxItem Content="24" />
<ComboBoxItem Content="25" />
<ComboBoxItem Content="26" />
<ComboBoxItem Content="27" />
<ComboBoxItem Content="28" />
<ComboBoxItem Content="29" />
<ComboBoxItem Content="30" />
<ComboBoxItem Content="31" />
<ComboBoxItem Content="32" />
<ComboBoxItem Content="33" />
<ComboBoxItem Content="34" />
<ComboBoxItem Content="35" />
<ComboBoxItem Content="36" />
<ComboBoxItem Content="37" />
<ComboBoxItem Content="38" />
<ComboBoxItem Content="39" />
<ComboBoxItem Content="40" />
<ComboBoxItem Content="41" />
<ComboBoxItem Content="42" />
<ComboBoxItem Content="43" />
<ComboBoxItem Content="44" />
<ComboBoxItem Content="45" />
<ComboBoxItem Content="46" />
<ComboBoxItem Content="47" />
<ComboBoxItem Content="48" />
<ComboBoxItem Content="49" />
<ComboBoxItem Content="50" />
<ComboBoxItem Content="51" />
<ComboBoxItem Content="52" />
<ComboBoxItem Content="53" />
<ComboBoxItem Content="54" />
<ComboBoxItem Content="55" />
<ComboBoxItem Content="56" />
<ComboBoxItem Content="57" />
<ComboBoxItem Content="58" />
<ComboBoxItem Content="59" />
</ComboBox>
<TextBlock HorizontalAlignment="Left" Width="70" Height="20" VerticalAlignment="Top" Margin="130,100">
勤務開始:
</TextBlock>
<TextBlock HorizontalAlignment="Left" Width="30" Height="20" VerticalAlignment="Top" Margin="260,100">
年
</TextBlock>
<TextBlock HorizontalAlignment="Left" Width="30" Height="20" VerticalAlignment="Top" Margin="360,100">
月
</TextBlock>
<TextBlock HorizontalAlignment="Left" Width="30" Height="20" VerticalAlignment="Top" Margin="460,100,0,1">
日
</TextBlock>
<TextBlock HorizontalAlignment="Left" Width="30" Height="20" VerticalAlignment="Top" Margin="560,100,0,1">
時
</TextBlock>
<TextBlock HorizontalAlignment="Left" Width="30" Height="20" VerticalAlignment="Top" Margin="660,100,0,1">
分
</TextBlock>
<ComboBox x:Name="EYear" HorizontalAlignment="Left" Width="50" Height="20" VerticalAlignment="Top" Margin="200,150,0,1" IsEditable="True">
<ComboBoxItem Content="2022" />
<ComboBoxItem Content="2023" />
<ComboBoxItem Content="2024" />
<ComboBoxItem Content="2025" />
<ComboBoxItem Content="2026" />
</ComboBox>
<ComboBox x:Name="EMonth" HorizontalAlignment="Left" Width="50" Height="20" VerticalAlignment="Top" Margin="300,150,0,1" IsEditable="True">
<ComboBoxItem Content="1" />
<ComboBoxItem Content="2" />
<ComboBoxItem Content="3" />
<ComboBoxItem Content="4" />
<ComboBoxItem Content="5" />
<ComboBoxItem Content="6" />
<ComboBoxItem Content="7" />
<ComboBoxItem Content="8" />
<ComboBoxItem Content="9" />
<ComboBoxItem Content="10" />
<ComboBoxItem Content="11" />
<ComboBoxItem Content="12" />
</ComboBox>
<ComboBox x:Name="EDay" HorizontalAlignment="Left" Width="50" Height="20" VerticalAlignment="Top" Margin="400,150,0,1" IsEditable="True">
<ComboBoxItem Content="1" />
<ComboBoxItem Content="2" />
<ComboBoxItem Content="3" />
<ComboBoxItem Content="4" />
<ComboBoxItem Content="5" />
<ComboBoxItem Content="6" />
<ComboBoxItem Content="7" />
<ComboBoxItem Content="8" />
<ComboBoxItem Content="9" />
<ComboBoxItem Content="10" />
<ComboBoxItem Content="11" />
<ComboBoxItem Content="12" />
<ComboBoxItem Content="13" />
<ComboBoxItem Content="14" />
<ComboBoxItem Content="15" />
<ComboBoxItem Content="16" />
<ComboBoxItem Content="17" />
<ComboBoxItem Content="18" />
<ComboBoxItem Content="19" />
<ComboBoxItem Content="20" />
<ComboBoxItem Content="21" />
<ComboBoxItem Content="22" />
<ComboBoxItem Content="23" />
<ComboBoxItem Content="24" />
<ComboBoxItem Content="25" />
<ComboBoxItem Content="26" />
<ComboBoxItem Content="27" />
<ComboBoxItem Content="28" />
<ComboBoxItem Content="29" />
<ComboBoxItem Content="30" />
<ComboBoxItem Content="31" />
</ComboBox>
<ComboBox x:Name="EHour" HorizontalAlignment="Left" Width="50" Height="20" VerticalAlignment="Top" Margin="500,150,0,1" SelectedIndex="-1" IsEditable="True" Visibility="Visible">
<ComboBoxItem Content="00" />
<ComboBoxItem Content="01" />
<ComboBoxItem Content="02" />
<ComboBoxItem Content="03" />
<ComboBoxItem Content="04" />
<ComboBoxItem Content="05" />
<ComboBoxItem Content="06" />
<ComboBoxItem Content="07" />
<ComboBoxItem Content="08" />
<ComboBoxItem Content="09" />
<ComboBoxItem Content="10" />
<ComboBoxItem Content="11" />
<ComboBoxItem Content="12" />
<ComboBoxItem Content="13" />
<ComboBoxItem Content="14" />
<ComboBoxItem Content="15" />
<ComboBoxItem Content="16" />
<ComboBoxItem Content="17" />
<ComboBoxItem Content="18" />
<ComboBoxItem Content="19" />
<ComboBoxItem Content="20" />
<ComboBoxItem Content="21" />
<ComboBoxItem Content="22" />
<ComboBoxItem Content="23" />
</ComboBox>
<ComboBox x:Name="EMinute" HorizontalAlignment="Left" Width="50" Height="20" VerticalAlignment="Top" Margin="600,150,0,1" IsEditable="True">
<ComboBoxItem Content="00" />
<ComboBoxItem Content="01" />
<ComboBoxItem Content="02" />
<ComboBoxItem Content="03" />
<ComboBoxItem Content="04" />
<ComboBoxItem Content="05" />
<ComboBoxItem Content="06" />
<ComboBoxItem Content="07" />
<ComboBoxItem Content="08" />
<ComboBoxItem Content="09" />
<ComboBoxItem Content="10" />
<ComboBoxItem Content="11" />
<ComboBoxItem Content="12" />
<ComboBoxItem Content="13" />
<ComboBoxItem Content="14" />
<ComboBoxItem Content="15" />
<ComboBoxItem Content="16" />
<ComboBoxItem Content="17" />
<ComboBoxItem Content="18" />
<ComboBoxItem Content="19" />
<ComboBoxItem Content="20" />
<ComboBoxItem Content="21" />
<ComboBoxItem Content="22" />
<ComboBoxItem Content="23" />
<ComboBoxItem Content="24" />
<ComboBoxItem Content="25" />
<ComboBoxItem Content="26" />
<ComboBoxItem Content="27" />
<ComboBoxItem Content="28" />
<ComboBoxItem Content="29" />
<ComboBoxItem Content="30" />
<ComboBoxItem Content="31" />
<ComboBoxItem Content="32" />
<ComboBoxItem Content="33" />
<ComboBoxItem Content="34" />
<ComboBoxItem Content="35" />
<ComboBoxItem Content="36" />
<ComboBoxItem Content="37" />
<ComboBoxItem Content="38" />
<ComboBoxItem Content="39" />
<ComboBoxItem Content="40" />
<ComboBoxItem Content="41" />
<ComboBoxItem Content="42" />
<ComboBoxItem Content="43" />
<ComboBoxItem Content="44" />
<ComboBoxItem Content="45" />
<ComboBoxItem Content="46" />
<ComboBoxItem Content="47" />
<ComboBoxItem Content="48" />
<ComboBoxItem Content="49" />
<ComboBoxItem Content="50" />
<ComboBoxItem Content="51" />
<ComboBoxItem Content="52" />
<ComboBoxItem Content="53" />
<ComboBoxItem Content="54" />
<ComboBoxItem Content="55" />
<ComboBoxItem Content="56" />
<ComboBoxItem Content="57" />
<ComboBoxItem Content="58" />
<ComboBoxItem Content="59" />
</ComboBox>
<TextBlock HorizontalAlignment="Left" Width="70" Height="20" VerticalAlignment="Top" Margin="130,150,0,1">
勤務終了:
</TextBlock>
<TextBlock HorizontalAlignment="Left" Width="30" Height="20" VerticalAlignment="Top" Margin="260,150,0,1">
年
</TextBlock>
<TextBlock HorizontalAlignment="Left" Width="30" Height="20" VerticalAlignment="Top" Margin="360,150,0,1">
月
</TextBlock>
<TextBlock HorizontalAlignment="Left" Width="30" Height="20" VerticalAlignment="Top" Margin="460,150,0,1">
日
</TextBlock>
<TextBlock HorizontalAlignment="Left" Width="30" Height="20" VerticalAlignment="Top" Margin="560,150,0,1">
時
</TextBlock>
<TextBlock HorizontalAlignment="Left" Width="30" Height="20" VerticalAlignment="Top" Margin="660,150,0,1">
分
</TextBlock>
<Button x:Name="copy" Content="copy" Width="60" Height="20" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="130,185,135,1" Click="Button_Clickp2"/>
<Button x:Name="NowTime" Content="現在時刻" Width="60" Height="20" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="130,185,545,1" Click="Button_Clickp2"/>
<Button x:Name="Registration" Content="登録" Width="60" Height="20" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="130,185,340,1" Click="Button_Clickp2"/>
<Button x:Name="End" Content="終了" Width="60" Height="20" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="130,370,135,1" Click="Button_Clickp2"/>
<Button x:Name="Load" Content="読み込み" Width="60" Height="20" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="130,370,230,1" Click="Button_Clickp2"/>
<ListBox x:Name="EditLog" HorizontalAlignment="Center" Height="130" Margin="0,220,0,0" VerticalAlignment="Top" Width="600"/>
</Grid>
</Page>
<Grid></Grid>
内にボタンやテキストブロックといった要素を記述しています。設計項の画面がこの記述で構成されていると考えるとわかりやすいかと思います。Page2にComboboxの内容を直接記載し長くなってしまったため折りたたんでいます。
スクリプト編
先ほどのXAMLはGUIを記述したものでしたが、こちらはメインの機能の処理部になります。
using System;
using System.IO;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
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 System.Windows.Navigation;
using System.Windows.Shapes;
namespace Work_Scheduler
{
/// <summary>
/// Page1.xaml の相互作用ロジック
/// </summary>
public partial class Page1 : Page
{
private static Page2 page2 = null;
public object SelectedFile = null;
public Page1()
{
InitializeComponent();
}
private void Button_Click(Object sender, RoutedEventArgs e)
{
switch ((sender as Button).Name)
{
case "Next":
SelectedFile = FileList.SelectedItem;
if (SelectedFile != null)
{
if (page2 == null)
{
page2 = new Page2(SelectedFile);
}
Debug.WriteLine(SelectedFile + "が選択されました");
this.NavigationService.Navigate(page2 , SelectedFile);
}
else
{
Console.Inlines.Clear();
Console.Inlines.Add(new Run("err:ファイルが選択されていません"));
Console.Foreground = Brushes.Red;
}
break;
case "Get":
try
{
FileList.Items.Clear();
string[] names = Directory.GetFiles(@"C:\WPFapps\Work Scheduler\Work Scheduler\files", "*.xlsx");
foreach (string name in names)
{
Debug.WriteLine(name);
FileList.Items.Add(name);
}
}
catch (Exception c)
{
Debug.WriteLine(c.ToString());
}
break;
}
}
}
}
Page1ではボタン操作でファイルの取得、画面遷移を行います。これはswitch文を用いて、xamlで配置したボタンの名前を識別して動作させています。ファイルの取得は取得したいフォルダのパスと拡張子(今回は.xlsx)を指定して行っています。また、画面遷移の際はListboxから選択されているファイル名を取得してPage2に渡しています。何も選択されていない場合はエラーを返し、画面遷移を行いません。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using System.Text;
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 System.Windows.Navigation;
using System.Windows.Shapes;
using Excel = Microsoft.Office.Interop.Excel;
namespace Work_Scheduler
{
/// <summary>
/// Page2.xaml の相互作用ロジック
/// </summary>
public partial class Page2 : Page
{
public Excel.Application ExcelApp;
public Excel.Workbook WorkbookBk;
public Page2(object FileName)
{
InitializeComponent();
Console2.Inlines.Clear();
Console2.Inlines.Add(new Run(FileName + "を編集中です\n終了する際は必ず下部の[終了]ボタンから行ってください"));
this.ExcelApp = new Excel.Application();
this.ExcelApp.Visible = false;
this.WorkbookBk = (Excel.Workbook)(ExcelApp.Workbooks.Open ((string)FileName,
Type.Missing,
Type.Missing,
Type.Missing,
Type.Missing,
Type.Missing,
Type.Missing,
Type.Missing,
Type.Missing,
Type.Missing,
Type.Missing,
Type.Missing,
Type.Missing,
Type.Missing,
Type.Missing));
}
private void Button_Clickp2(Object sender, RoutedEventArgs e)
{
switch ((sender as Button).Name)
{
case "copy": //開始時刻の値を終了時刻欄にコピー
string StartY = this.Year.Text;
string StartMo = this.Month.Text;
string StartD = this.Day.Text;
string StartH = this.Hour.Text;
string StartMi = this.Minute.Text;
Debug.WriteLine(StartY + "年" + StartMo + "月" + StartD + "日" + StartH + "時" + StartMi + "分をコピー");
this.EYear.Text = StartY;
this.EMonth.Text = StartMo;
this.EDay.Text = StartD;
this.EHour.Text = StartH;
this.EMinute.Text = StartMi;
break;
case "NowTime": //開始時刻欄に現在時刻を反映
var dt = DateTime.Now;
var arr = dt.ToString("yyyy年MM月dd日HH時mm分"); //取得したDateTimeをstring型に変換
var dtList = Regex.Split(arr, "年|月|日|時|分"); //変換した値を分割して配列に収納する
this.Year.Text = dtList[0];
this.Month.Text = dtList[1];
this.Day.Text = dtList[2];
this.Hour.Text = dtList[3];
this.Minute.Text = dtList[4];
Debug.WriteLine("現在時刻:" + arr + "を反映しました。");
break;
case "Registration": //入力された値をセルに記入する
if ((Year.SelectedItem != null || string.IsNullOrEmpty(Year.Text) == false) && (EYear.SelectedItem != null || string.IsNullOrEmpty(EYear.Text) == false) && (Month.SelectedItem != null || string.IsNullOrEmpty(Month.Text) == false) && (EMonth.SelectedItem != null || string.IsNullOrEmpty(EMonth.Text) == false) && (Day.SelectedItem != null || string.IsNullOrEmpty(Day.Text) == false) && (EDay.SelectedItem != null || string.IsNullOrEmpty(EDay.Text) == false) && (Hour.SelectedItem != null || string.IsNullOrEmpty(Hour.Text) == false) && (EHour.SelectedItem != null || string.IsNullOrEmpty(EHour.Text) == false) && (Minute.SelectedItem != null || string.IsNullOrEmpty(Minute.Text) == false) && (EMinute.SelectedItem != null || string.IsNullOrEmpty(EMinute.Text) == false)) //空欄がないか調べ、存在した場合後続処理をパスする
{
string RegY = this.Year.Text;
string RegMo = this.Month.Text;
string RegD = this.Day.Text;
string RegH = this.Hour.Text;
string RegMi = this.Minute.Text;
string RegEY = this.EYear.Text;
string RegEMo = this.EMonth.Text;
string RegED = this.EDay.Text;
string RegEH = this.EHour.Text;
string RegEMi = this.EMinute.Text;
if (DatingChecker(RegY, RegMo, RegD) == true && DatingChecker(RegEY, RegEMo, RegED) == true) //日付が存在するかチェックし、存在しなければ後続処理を行わない
{
string StartTime = (RegY + "/" + RegMo + "/" + RegD + " " + RegH + ":" + RegMi);
DateTime STime = DateTime.Parse(StartTime);
string EndTime = (RegEY + "/" + RegEMo + "/" + RegED + " " + RegEH + ":" + RegEMi);
DateTime ETime = DateTime.Parse(EndTime);
int WorkTime = TimeComparer(STime, ETime);
this.EditLog.Items.Insert(0, "勤務時刻:" + RegY + "年" + RegMo + "/" + RegD + " " + RegH + ":" + RegMi + " ‐ " + RegEY + "年" + RegEMo + "/" + RegED + " " + RegEH + ":" + RegEMi + "を登録しました(勤務時間" + WorkTime / 3600 + "時間)");
Excel.Worksheet EditSheet = (Excel.Worksheet)this.WorkbookBk.Sheets[1];
string defaultRange = vsSearch("Day1");
EditSheet.Range[RangeConverter(defaultRange, RegD, 0)].Value = RegH + ":" + RegMi;
if (RegD != RegED) //勤務開始日と終了日が異なる場合の処理
{
int HourWorkTime = WorkTime / 3600;
int RegNewEH = HourWorkTime + int.Parse(RegH);
RegEH = "'" + RegNewEH.ToString();
RegED = RegD;
}
EditSheet.Range[RangeConverter(defaultRange, RegED, 1)].Value = RegEH + ":" + RegEMi;
EditSheet.Range[RangeConverter(defaultRange, RegD , 2)].Value = statusSearch("Break:");
this.WorkbookBk.Save();
}
else
{
this.EditLog.Items.Insert(0, "日付の値が不正です");
}
}
else
{
this.EditLog.Items.Insert(0, "空欄があります");
}
break;
case "End": //アプリケーションの終了(Excelアプリケーションを終了する)
this.WorkbookBk.Close();
Application.Current.Shutdown();
break;
case "Load": //files配下のstatus.txtをシートに反映する
Excel.Worksheet EditSheetSt = (Excel.Worksheet)this.WorkbookBk.Sheets[1];
string RangeMo = vsSearch("Month:");
string RangeNa = vsSearch("Name:");
string RangeCont = vsSearch("Contract:");
string RangeWo = vsSearch("WorkPlace:");
string RangeSt = vsSearch("Start:");
string RangeEn = vsSearch("End:");
EditSheetSt.Range[RangeMo].Value = statusSearch("Month:");
EditSheetSt.Range[RangeNa].Value = statusSearch("Name:");
EditSheetSt.Range[RangeCont].Value = statusSearch("Contract:");
EditSheetSt.Range[RangeWo].Value = statusSearch("WorkPlace:");
EditSheetSt.Range[RangeSt].Value = statusSearch("Start:");
EditSheetSt.Range[RangeEn].Value = statusSearch("End:");
this.WorkbookBk.Save();
this.EditLog.Items.Insert(0, "statusファイルをロードしました");
break;
}
}
public int TimeComparer(DateTime STime , DateTime ETime) //UnixTimeを用いて開始時刻と終了時刻の差を求める
{
int diffTime = GetUnixTime(ETime) - GetUnixTime(STime);
return diffTime;
}
public static int GetUnixTime(DateTime timeStamp) //UnixTimeの取得処理
{
var unixTimestamp = (int)(timeStamp.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;
return unixTimestamp;
}
public static string vsSearch(string SearchValue) //vs.txtよりキーを検索し戻り値として対応する文字列を与える
{
StreamReader sr = new StreamReader(@"C:\WPFapps\Work Scheduler\Work Scheduler\files\vs.txt");
string line = "";
string GetValue = null;
Debug.WriteLine(SearchValue);
while ((line = sr.ReadLine())!= null)
{
int num = line.IndexOf(SearchValue);
if (num >= 0)
{
Debug.WriteLine("sucsess");
string sv = (string)line;
GetValue = sv.Replace(SearchValue, "");
}
}
return GetValue;
}
public static string statusSearch(string SearchValueSt) //status.txtよりキーを検索し戻り値として対応する文字列を与える
{
StreamReader srSt = new StreamReader(@"C:\WPFapps\Work Scheduler\Work Scheduler\files\status.txt");
string lineSt = "";
string GetValueSt = null;
Debug.WriteLine(SearchValueSt);
while ((lineSt = srSt.ReadLine()) != null)
{
int numSt = lineSt.IndexOf(SearchValueSt);
if (numSt >= 0)
{
Debug.WriteLine("sucsess");
string svSt = (string)lineSt;
GetValueSt = svSt.Replace(SearchValueSt, "");
}
}
return GetValueSt;
}
public static string RangeConverter(string CellRange , string Time , int StartFlag) //vs.txtの値と日付からセル番地を計算
{
var NumAlpha = new Regex("(?<Alpha>[a-zA-Z]*)(?<Numelic>[0-9]+)"); //セル番地の値を分割
var match = NumAlpha.Match(CellRange);
int writeTime = int.Parse(match.Groups["Numelic"].Value); //数値を抜き出し
Debug.WriteLine(writeTime);
int newTime = writeTime + int.Parse(Time) - 1; //数値に日付を足して書き込むセルを指定
string alphaNo = match.Groups["Alpha"].ToString();
int index = 0;
string alphabet = null;
if (StartFlag == 1 || StartFlag == 2) //終業時刻の場合に列を1つずらす
{
index += StartFlag;
for (int i = 0; i < alphaNo.Length; i++)
{
int num = Convert.ToChar(alphaNo[alphaNo.Length - i - 1]) - 64;
index += (int)(num * Math.Pow(26, i));
}
index--;
do
{
alphabet = Convert.ToChar(index % 26 + 0x41) + alphabet;
}
while
((index = index / 26 - 1) != -1);
}
else
{
alphabet = alphaNo;
}
string WriteRange = alphabet + newTime.ToString();
Debug.WriteLine(WriteRange);
return WriteRange;
}
public static bool DatingChecker(string NewYear , string NewMonth , string NewDay) //日付の整合性をチェック
{
if((NewMonth == "04" || NewMonth == "06" || NewMonth == "09" || NewMonth == "11") && int.Parse(NewDay) <= 30)
{
return true;
}
else if(NewMonth == "02" && int.Parse(NewDay) <= 28)
{
return true;
}
else if(NewMonth == "02" && int.Parse(NewYear) % 4 == 0 && !(int.Parse(NewYear) % 100 == 0 && int.Parse(NewYear) % 400 != 0))
{
return true;
}
else if(int.Parse(NewDay) <= 31 )
{
return false;
}
else
{
return false;
}
}
}
}
こちらが実際に編集等を行うPage2です。長いので一部メソッドは抜粋して紹介します。
まず最初の処理として、Page2メソッドでPage1から編集するファイルのパスを受け取り、Excelを起動しています。Excel関連の操作はこちら▼を大変参考にさせていただきました。ありがとうございます。
次のButton_Clickp2メソッドではPage1と同様に押下されたボタンの名前によって処理を識別しています。コメントアウトでも記載していますが、登録(Registration)ボタンは押下後にComboboxが空欄でないか、日付の値は正常値であるかを判定しています。後述しますが、日付を跨いだ勤務の場合シートの設定で特殊な記載をしなければならないのでその処理も行っています。
public static bool DatingChecker(string NewYear , string NewMonth , string NewDay) //日付の整合性をチェック
{
if((NewMonth == "04" || NewMonth == "06" || NewMonth == "09" || NewMonth == "11") && int.Parse(NewDay) <= 30)
{
return true;
}
else if(NewMonth == "02" && int.Parse(NewDay) <= 28)
{
return true;
}
else if(NewMonth == "02" && int.Parse(NewYear) % 4 == 0 && !(int.Parse(NewYear) % 100 == 0 && int.Parse(NewYear) % 400 != 0))
{
return true;
}
else if(int.Parse(NewDay) <= 31 )
{
return false;
}
else
{
return false;
}
}
こちらは日付が存在するかを確認するDatingCheckerメソッドです。引数として年月日を受け取り、boolean型で結果を返しています。行っていることは単純で、最初にその月に存在する日付であるか(4,6,9,11月)、次に年をチェックして閏年であるか判定し、2月の日付が正しいか判定しています。これを勤務開始日、終了日ともに行い、両者がtrueだった場合のみ登録処理に移行しています。
public int TimeComparer(DateTime STime , DateTime ETime) //UnixTimeを用いて開始時刻と終了時刻の差を求める
{
int diffTime = GetUnixTime(ETime) - GetUnixTime(STime);
return diffTime;
}
public static int GetUnixTime(DateTime timeStamp) //UnixTimeの取得処理
{
var unixTimestamp = (int)(timeStamp.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;
return unixTimestamp;
}
こだわりポイントとして、Page2下部のログに勤務開始時刻と終了時刻の差を表示したかったので2つの値を比較するメソッドを作成しました。Comboboxの内容はstring型ですが、一度DateTime型に変換してTimeComparerメソッドに渡しています。日付の差を求める方法としてはUnixTimeを使用しました。一度のTimeComparerメソッドの作動で複数回起動する必要があるため別のメソッドに分割しています。
public static string RangeConverter(string CellRange , string Time , int StartFlag) //vs.txtの値と日付からセル番地を計算
{
var NumAlpha = new Regex("(?<Alpha>[a-zA-Z]*)(?<Numelic>[0-9]+)"); //セル番地の値を分割
var match = NumAlpha.Match(CellRange);
int writeTime = int.Parse(match.Groups["Numelic"].Value); //数値を抜き出し
Debug.WriteLine(writeTime);
int newTime = writeTime + int.Parse(Time) - 1; //数値に日付を足して書き込むセルを指定
string alphaNo = match.Groups["Alpha"].ToString();
int index = 0;
string alphabet = null;
if (StartFlag == 1 || StartFlag == 2) //終業時刻の場合に列を1つずらす
{
index += StartFlag;
for (int i = 0; i < alphaNo.Length; i++)
{
int num = Convert.ToChar(alphaNo[alphaNo.Length - i - 1]) - 64;
index += (int)(num * Math.Pow(26, i));
}
index--;
do
{
alphabet = Convert.ToChar(index % 26 + 0x41) + alphabet;
}
while
((index = index / 26 - 1) != -1);
}
else
{
alphabet = alphaNo;
}
string WriteRange = alphabet + newTime.ToString();
Debug.WriteLine(WriteRange);
return WriteRange;
}
このRangeConverterメソッドではvs.txtから取得した値をもとに、登録したい日付に応じてセル番地を計算するメソッドになります。弊社勤務表では(というか大体の勤務表がそうだと思う)日付が列に対応しており、日付の数字分アルファベットを進める必要があるためその処理を行っています。
参考▼
アルファベット部は一度数値に変換し加算処理を行ったのちに数値からアルファベットに戻しています。
public static string vsSearch(string SearchValue) //vs.txtよりキーを検索し戻り値として対応する文字列を与える
{
StreamReader sr = new StreamReader(@"C:\WPFapps\Work Scheduler\Work Scheduler\files\vs.txt");
string line = "";
string GetValue = null;
Debug.WriteLine(SearchValue);
while ((line = sr.ReadLine())!= null)
{
int num = line.IndexOf(SearchValue);
if (num >= 0)
{
Debug.WriteLine("sucsess");
string sv = (string)line;
GetValue = sv.Replace(SearchValue, "");
}
}
return GetValue;
}
public static string statusSearch(string SearchValueSt) //status.txtよりキーを検索し戻り値として対応する文字列を与える
{
StreamReader srSt = new StreamReader(@"C:\WPFapps\Work Scheduler\Work Scheduler\files\status.txt");
string lineSt = "";
string GetValueSt = null;
Debug.WriteLine(SearchValueSt);
while ((lineSt = srSt.ReadLine()) != null)
{
int numSt = lineSt.IndexOf(SearchValueSt);
if (numSt >= 0)
{
Debug.WriteLine("sucsess");
string svSt = (string)lineSt;
GetValueSt = svSt.Replace(SearchValueSt, "");
}
}
return GetValueSt;
}
こちらが設計項に書いた「異なる環境でも動作させるための工夫」になります。それぞれ、vs.txt、status.txtファイルを参照して値を取り出しています。vs.txtからはセル番地の情報を、status.txtからは名前や勤務時間など基本的な情報を文字検索によって取得します。
参考▼
勤務表作成時になるべくExcelファイルを直接編集することを避けたかったので名前等を自動挿入してくれる処理を実装してみました。では、実際に各ファイルの中身を見ていきましょう。
※個人情報を一部隠しています
共通の値はそれぞれvs.txtはセル番地でstatus.txtは入力する値を表しています。
固有の値ですが、vs.txtの[Day1]は1日の勤務開始時刻を入力するセル番地を、status.txtの[Break]は休憩時間となっており、休憩時間は勤務時間登録時にシートの勤務終了時刻の右側のセルに反映されます。
実際に勤務表に記入してみた
それでは実際にこのアプリケーションを用いて勤務表に記入していきましょう。
まず、弊社の勤務表はこんな感じになっています。
vs.txtは先ほどのもので問題ありませんので次に、status.txtを次のように変更。
Work Scheduler.exeを起動し、編集対象のファイルを選択し[次へ]、まずはstatus.txtを読み込んでみましょう。
ロードに成功したので一度アプリケーションを終了し、Excelファイルを確認してみます。
status.txtで設定していた値がきちんと反映されていることが確認できました!
再度アプリケーションを起動し、実際に勤怠を登録してみます。
まずは平日9-18時の勤怠を。続いて、日付を跨ぐ勤怠として、22時-翌7時の勤務を登録してみます。
弊社勤務表は日付を跨ぐ勤務の場合、勤務終了時刻は開始時刻に足し算で表記し先頭にシングルクォートを付けるというシステムになっていますが正常に反映されるでしょうか?
※日付を跨ぐ処理はPage2.xaml.csのButton_Clickp2メソッドのcase "Registration"に記載されています。
if (RegD != RegED) //勤務開始日と終了日が異なる場合の処理
{
int HourWorkTime = WorkTime / 3600;
int RegNewEH = HourWorkTime + int.Parse(RegH);
RegEH = "'" + RegNewEH.ToString();
RegED = RegD;
}
登録したのでシートで確認してみます。
正しく反映されています。ちゃんと先頭にシングルクォートも付加されていますね。
上手く動作することが確認できたので実際に自分の今月の勤務表を作成してみました。
今月は一度に作成しましたが、アプリケーションを用いることで毎日のルーティーンに組み込みやすく、作成を忘れていて月末に慌てて編集…という事態に陥りづらいのではないかと思っています。
反省や今後の展望など
・Windowsの環境しかなく、MacOSで動作するかわからない
元々WindowsOSで動作させることを目標に作成したため要件は満たせているが、エンジニアさんにはMacを使用している人も多いイメージがあるためMacOSでも動作すれば広く使って貰えるのでは、と思っています。
・有休や祝日などは直接編集が必要
有休使用時や祝日などはそれを明記する欄がありますが、その欄の編集に対応していないためExcelを起動して編集する必要があります。ここまで細かく設定するとほぼ弊社勤務表の編集に特化したツールになってしまうと思い、実装を見送りました。
・「平日機能」があってもよかった
月~金の定時時間を1か月分一気に登録する機能があってもよかったなと思いました。これ、実装できれば大幅な時短になると思うのでいずれ実装してみたいと思っています。
さいごに
今回は、見やすいスクリプトを書くことを目標として取り組んでみました。複数回行う処理をメソッドに分割したり、変数を増やしすぎないことを意識してみましたが、どちらも少し増やしすぎたかなといった感想です。C#は以前Unityでゲームを作成した経験がありましたが、クラスもメソッドもよくわからない状態だったのでかなり冗長化してしまいました。そういった部分では今回はコードが長くなりすぎないことを意識することができてかなり満足したかなと思っています。最後までお読みいただきありがとうございました。
参考にしたもの
本文中では紹介しきれませんでしたが大変参考にさせて頂いたものを紹介します。