バーコード読み取りソフト

Excelでバーコードを読み取りたいと思ったけど、dllをsystem32へコピーする必要があったりとちょっと今の環境ではできないことだったので、専用のexeを作ってVectorで公開した。

  https://www.vector.co.jp/soft/winnt/writing/se523178.html


使い方はとっても簡単。Zxing.exeの引数にファイルパスを指定するだけ。

例)

 ・コマンドプロンプトを起動

 ・Zxing.exeのあるフォルダへ移動

 ・Zxing.exe c:\users\ユーザー名\DeskTop\バーコード画像.jpg

 ・Result.txtに結果が出力される


トリミングをする必要があるので、Excelのトリミング機能を使って、トリミングした後の画像を一時的にtemp.bmpとかに保存しておいてその画像をZxing.exeに渡してあげる。


Excelで簡易ガントチャートツール

仕事でプロジェクトツールが全く使われていなく、導入する予定もない部署への応援が決定した。

取り急ぎ、ガントチャートを作れるソフトが欲しかったのでExcelで作れるか試してみた。まぁちゃんとしたソフトがインストールできればそれでよかったんだけど、難しそうなので自作でしのげればと思う。

作成途中だけど、日付を入れるとその日の色が変わる程度なら作れたので一旦、ここまでとしておこうと思う。正直どんな管理項目が必要かもわからないので、そのあたりは融通が利くように列名の定義をできるようにしておいた。さて、必要そうな機能(まぁ色変えるだけなんだけど)の実装は終わったのであとはコードのコピペして完成品を作ろう。


<概要>
 ・処理開始時にメインシートに定義しておいた、色を変えたい日付範囲の項目名を探す
 ・列があとからどんどん追加されていくので「ガントチャート」部分の開始列が分かるように「【ガントチャート開始】」という文字列を入れておく。(消さないでね・・・)
 ・日付セルの場合は加算、減算ができるので基準日から開始日、終了日からの差を見ればどの列範囲かもわかるのでそこの色を変える。
 ・キーワードの設定(〆切日、その他色々)もできればいいな。(願望)
 ・範囲を複数持たせていろんな意味の色を塗れるようにする。発注期間の色、使用期限の色、利用期間の色、などなど(できればいいな)

<プログラム とりあえず4行目の色を変えるだけ>

Private fintBetween1Start As Integer
Private fintBetween1End As Integer
Private fintBetween2Start As Integer
Private fintBetween2End As Integer
Private fintBetween3Start As Integer
Private fintBetween3End As Integer
Private fintBetween4Start As Integer
Private fintBetween4End As Integer
Private fintBetween5Start As Integer
Private fintBetween5End As Integer
Private fdatePrjStartDate As Date

Private Const SHEETNAME_DATA As String = "データ"
Private Const SHEETNAME_MAIN As String = "メイン"
Private Const HeaderRow As Integer = 3
Private Const DataStartRow As Integer = 4


Private Const FINT_MAX_COLUMN_IN_DATE As Integer = 1500

'メインシート
Private Const FINT_KEYWORD_START_ROW As Integer = 6
Private Const FINT_KEYWORD_ITEMNAME_COLUMN As Integer = 12
Private Const FINT_KEYWORD_ITEMVALUE_COLUMN As Integer = 13

'データシート
Private fintGanttChartStartColumn As Integer

Private Const KEY_WORD_MAX_COUNT As Integer = 100 '何件でもいいがとりあえずの総件数

Private Sub Main()

    Call InitializeSetting
    
    Call InitializeRowStatus(4)
    
    Call SetRow(4)

End Sub


Private Sub SetRow(ByVal lngRow As Long)
    Dim lngDiff As Long
    Dim lngEndDiff As Long
    Dim rng As Range
    Dim rngColor As Range
    
    '範囲1の内容を色塗り
    lngDiff = DateDiff("d", fdatePrjStartDate, Sheets(SHEETNAME_DATA).Cells(lngRow, fintBetween1Start))
    lngEndDiff = DateDiff("d", fdatePrjStartDate, Sheets(SHEETNAME_DATA).Cells(lngRow, fintBetween1End))
    Set rngColor = Sheets(SHEETNAME_MAIN).Range("F7")
    Set rng = Sheets(SHEETNAME_DATA).Range(Sheets(SHEETNAME_DATA).Cells(lngRow, fintGanttChartStartColumn + lngDiff), Sheets(SHEETNAME_DATA).Cells(lngRow, fintGanttChartStartColumn + lngEndDiff))
    With rng.Interior
        .Color = rngColor.Interior.Color
        .TintAndShade = rngColor.Interior.TintAndShade
        .PatternTintAndShade = rngColor.Interior.PatternTintAndShade
    End With

    'キーワードを設定(作成中)

'    Dim i, j As Long
'    Dim strKeyWord As String
'    Dim strCellValue As String
'    Dim strColumns As String
'    Dim strColumnArr() As String
'
'    For i = 0 To 100
'        strKeyWord = Sheets(SHEETNAME_MAIN).Cells(i + FINT_KEYWORD_START_ROW, FINT_KEYWORD_ITEMVALUE_COLUMN).Value
'        If strKeyWord = "" Then Exit Sub
'        strColumns = GetSearchStringCell(strKeyWord, True, CStr(lngRow) & ":" & CStr(lngRow), SHEETNAME_DATA)
'
'        strColumnArr = Split(strColumns, ",")
'        For j = 0 To UBound(strColumnArr)
'            If Sheets(SHEETNAME_DATA).Cells(lngRow, strColumnArr(j)).Value = strKeyWord Then
'                Sheets(SHEETNAME_DATA).Cells(lngRow, strColumnArr(j)).Value = ""
'            End If
'        Next
'    Next


End Sub
Private Sub InitializeSetting()

    Dim strGanttChartStartDay As String
    strGanttChartStartDay = GetSearchStringCell("【ガントチャート開始】", True, "1:1", SHEETNAME_DATA)

    fintGanttChartStartColumn = CInt(strGanttChartStartDay)
    fdatePrjStartDate = Sheets(SHEETNAME_MAIN).Range("F4").Value
    fintBetween1Start = GetColumnNo(Sheets(SHEETNAME_MAIN).Range("F5").Value)
    fintBetween1End = GetColumnNo(Sheets(SHEETNAME_MAIN).Range("F6").Value)
    
End Sub

Private Function GetColumnNo(ByVal strColName As String)
    Dim i As Long
    GetColumnNo = 0
    For i = 1 To 100
        If Sheets(SHEETNAME_DATA).Cells(HeaderRow, i).Value = strColName Then
            GetColumnNo = i
            Exit Function
        End If
    Next
End Function
Private Function GetStartGanntChartColumn(ByVal strColName As String)
    Dim i As Long
    GetColumnNo = 0
    For i = 1 To 300
        If Sheets(SHEETNAME_DATA).Cells(1, i).Value = strColName Then
            GetColumnNo = i
            Exit Function
        End If
    Next
End Function


'---------------------------------------------------------
' 指定行の初期化処理を行う
'---------------------------------------------------------
Private Sub InitializeRowStatus(ByVal lngRow As Long)
    
    'Rowの背景色を全て削除
    Call ClearBackgroundInRow(lngRow)

    'キーワードを削除
    Call ClearKeyWordInRow(lngRow)
    
End Sub


Private Sub ClearBackgroundInRow(ByVal lngRow As Long)
    Dim rng As Range
    
    Dim strRange As String
    
    Set rng = Sheets(SHEETNAME_DATA).Range(Cells(lngRow, fintGanttChartStartColumn), Sheets(SHEETNAME_DATA).Cells(lngRow, FINT_MAX_COLUMN_IN_DATE))
    
    '背景色削除
    With rng.Interior
        .Pattern = xlNone
        .TintAndShade = 0
        .PatternTintAndShade = 0
    End With
    
End Sub

'---------------------------------------------------------
' キーワードとして設定されている内容をすべて削除する
'---------------------------------------------------------
Private Sub ClearKeyWordInRow(ByVal lngRow As Long)

    Dim i, j As Long
    Dim strKeyWord As String
    Dim strCellValue As String
    Dim strColumns As String
    Dim strColumnArr() As String
    
    For i = 0 To 100
        strKeyWord = Sheets(SHEETNAME_MAIN).Cells(i + FINT_KEYWORD_START_ROW, FINT_KEYWORD_ITEMVALUE_COLUMN).Value
        If strKeyWord = "" Then Exit Sub
        strColumns = GetSearchStringCell(strKeyWord, True, CStr(lngRow) & ":" & CStr(lngRow), SHEETNAME_DATA)
        
        strColumnArr = Split(strColumns, ",")
        For j = 0 To UBound(strColumnArr)
            If Sheets(SHEETNAME_DATA).Cells(lngRow, strColumnArr(j)).Value = strKeyWord Then
                Sheets(SHEETNAME_DATA).Cells(lngRow, strColumnArr(j)).Value = ""
            End If
        Next
    Next

End Sub


'---------------------------------------------------------
' 日付セルを検索
' シート名と行か列位置を指定し検索する。該当した行、列位置をカンマ区切りで返す。
'  1件ずつループしながらIf関数で1行ずつ判定するより圧倒的に高速
'  複数条件は対応外なので、この関数で該当行を絞ったうえで、If関数で詳細なチェックをかけたほうが高速になる
' 利用例)GetSearchDateCell("2020/08/01",False,"A:A","データ")
' 戻り値例)4,15,20
'---------------------------------------------------------
Public Function GetSearchDateCell(ByVal datValue As Date, ByVal blnSearchInRow As Boolean, ByVal strRange As String, ByVal strSheetName As String) As String
    Dim rng As Range
    Dim adr As String
    Dim strResult As String
    GetSearchDateCell = ""
  
    If blnSearchInRow = True Then
        Set rng = Sheets(strSheetName).Rows(strRange).Find(datValue)
    Else
        Set rng = Sheets(strSheetName).Columns(strRange).Find(datValue)
    End If
  
    If rng Is Nothing Then
        Exit Function
    Else
        adr = rng.Address
        If blnSearchInRow = True Then
            strResult = rng.Column
        Else
            strResult = rng.Row
        End If
    End If

    Do
      
    If blnSearchInRow = True Then
        Set rng = Sheets(strSheetName).Rows(strRange).FindNext(After:=rng)
    Else
        Set rng = Sheets(strSheetName).Columns(strRange).FindNext(After:=rng)
    End If
    
    If rng.Address = adr Then
            Exit Do
        Else
            If blnSearchInRow = True Then
                strResult = strResult & "," & rng.Column
            Else
                strResult = strResult & "," & rng.Row
            End If
        End If
    Loop

    GetSearchDateCell = strResult

End Function


'---------------------------------------------------------
' 文字列セルを検索
' シート名と行か列位置を指定し検索する。該当した行、列位置をカンマ区切りで返す。
'  1件ずつループしながらIf関数で1行ずつ判定するより圧倒的に高速
'  複数条件は対応外なので、この関数で該当行を絞ったうえで、If関数で詳細なチェックをかけたほうが高速になる
' 利用例)GetSearchDateCell("2020/08/01",False,"A:A","データ")
' 戻り値例)4,15,20
'---------------------------------------------------------
Public Function GetSearchStringCell(ByVal strValue As String, ByVal blnSearchInRow As Boolean, ByVal strRange As String, ByVal strSheetName As String) As String
    Dim rng As Range
    Dim adr As String
    Dim strResult As String
    GetSearchStringCell = ""
  
    If blnSearchInRow = True Then
        Set rng = Sheets(strSheetName).Rows(strRange).Find(strValue)
    Else
        Set rng = Sheets(strSheetName).Columns(strRange).Find(strValue)
    End If
  
    If rng Is Nothing Then
        Exit Function
    Else
        adr = rng.Address
        If blnSearchInRow = True Then
            strResult = rng.Column
        Else
            strResult = rng.Row
        End If
    End If

    Do
      
    If blnSearchInRow = True Then
        Set rng = Sheets(strSheetName).Rows(strRange).FindNext(After:=rng)
    Else
        Set rng = Sheets(strSheetName).Columns(strRange).FindNext(After:=rng)
    End If
    
    If rng.Address = adr Then
            Exit Do
        Else
            If blnSearchInRow = True Then
                strResult = strResult & "," & rng.Column
            Else
                strResult = strResult & "," & rng.Row
            End If
        End If
    Loop

    GetSearchStringCell = strResult

End Function









DataGridにコンボボックスを表示する

 WPFのDataGridでコンボボックスを表示するのにちょっと詰まってしまったのでメモ。

<結論>
 バインドするデータのクラスにコンボボックスの選択肢のインスタンスを作る
 
<経緯>
 1.自動化ツール(以下、RPA)を作ろう
 2.どんな動作をするか定義をしよう
    例)
      ・マウスを(X,Y)=(20,200)へ移動
      ・クリック
      ・3秒待機
 3.動作定義のためのクラスを定義
  
        public ViewModel ThisViewModel;
        public MainWindow()
        {
            InitializeComponent();

            this.ThisViewModel = new ViewModel();

            this.ThisViewModel.Title = "タイトル";
            this.ThisViewModel.Actions.Add(new Action() { ActionName = "マウス移動", SetValue = "20,200" });
            this.ThisViewModel.Actions.Add(new Action() { ActionName = "クリック", SetValue = "" });
            this.ThisViewModel.Actions.Add(new Action() { ActionName = "待機", SetValue = "2" });


            this.DataContext = this.ThisViewModel;
        }

        public class ViewModel : ModelBase
        {
            private string _title;
            public string Title
            {
                get { return this._title; }
                set { this._title = value; NotifyPropertyChanged(); }
            }

            public ObservableCollection<Action> Actions { get; set; }
            public ViewModel()
            {
                this.Actions = new ObservableCollection<Action>();
            }
        }

        #region PropertyChangeイベントを通知する処理を実装したベースクラス
        public class ModelBase : INotifyPropertyChanged
        {
            #region インターフェイス実装イベント
            public event PropertyChangedEventHandler PropertyChanged;
            public void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
            {
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                }
            }
            #endregion
        }
        #endregion

        /// <summary>
        /// 動作定義を登録するクラス
        /// </summary>
        public class Action: ModelBase
        {
            private string _actionName;
            public string ActionName
            {
                get { return this._actionName; }
                set
                {
                    this._actionName = value;
                    SetSelectedItem();
                    NotifyPropertyChanged();
                }
            }

            private string _setValue;
            public string SetValue
            {
                get { return this._setValue; }
                set
                {
                    this._setValue = value; NotifyPropertyChanged();
                }
            }

            public ComboBoxItem SelectedItem { get; set; }

            public void SetSelectedItem()
            {
                foreach (var item in ComboItems)
                {
                    if(item.Name==this.ActionName)
                    {
                        this.SelectedItem = item;
                        break;
                    }
                }
            }

            public ObservableCollection<ComboBoxItem> ComboItems { get; set; }

            public Action()
            {
                this.ComboItems = new ObservableCollection<ComboBoxItem>();
                this.ComboItems.Add(new ComboBoxItem() { Name = "マウス移動", Value = "001" });
                this.ComboItems.Add(new ComboBoxItem() { Name = "クリック", Value = "002" });
                this.ComboItems.Add(new ComboBoxItem() { Name = "待機", Value = "002" });
            }
        }
      
 4.下記のXAMLにバインド
    <Grid>
        <Canvas>
            <TextBox Canvas.Left="10" Canvas.Top="26" Text="{Binding Title}" TextWrapping="Wrap" Width="400"/>

            <DataGrid Height="217" Width="400" Canvas.Left="10" Canvas.Top="60" ItemsSource="{Binding Actions}" AutoGenerateColumns="False">
                <DataGrid.Columns>
                    <DataGridTemplateColumn Header="動作名" Width="150">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <ComboBox ItemsSource="{Binding ComboItems}" Width="140"
                                                 SelectedItem="{Binding SelectedItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                                                 DisplayMemberPath="Name" />
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTextColumn Width="150" Header="引数" Binding="{Binding SetValue}"></DataGridTextColumn>
                </DataGrid.Columns>
            </DataGrid>
        </Canvas>
    </Grid>

 5.結果について
    DataGridのRowにバインドされているデータにコンボボックスの選択肢のインスタンスを持っておく必要がある。(検証不足?)
    ってことはDataTableをバインドして一瞬でできたぜヒャッハー!
    ってのは無理ってことなのかなぁ。まぁ入力値のチェックとかを実装することを考えるとどうしてもクラスを一つ作らざるをえないのかな・・・。
    一覧コピー的なことを一括でやろうとすると少し手間かもしれない。

 6.新規行のコンボボックスでてんやわんや
   新規行のコンボボックスにはNewItemPlaceHolderってのがバインドされているため、コンボボックスの選択肢が表示されない。
   この例でいうと「引数」列に何らかの値を入れて、行を確定させてあげると選択肢がわいてくる。何とかしたいなーって思ったので下記のサイトを見つけた。
   https://social.msdn.microsoft.com/Forums/ja-JP/7b1630de-0982-4191-bd73-3cd060e89439/wpfdatagriddatagridtemplatecolumninitializingnewitemraise?forum=wpfja
   このサイトの通りにやると一瞬ちょっとずれは生じるけどなんとかなった。
  
 7.補足
   一覧の件数がめっちゃ多い場合については表示が遅くなることがある。
   DataTemplateで作成している列については、ComboBoxがひたすら表示されているだけなので、DataGridのEditモードに移行しなくても触ることができるのが便利な反面、描画速度には問題が出ることがあるかもしれない。
   まぁ一覧を1000件表示するぐらいなら問題ないだろうし、そんなに入力できるわけないからいいと思うんだけども。
   もしどうしてもしないといけなくなったら、CellTemplateにはTextBlockでも表示しておいて、CellEditingTemplateにComboBoxを配置してあげればいい。
   DataGridTextColumnと同じでDataRowが編集モードになれば触るようになる。
    

VBA 画面タイトルを指定しIEを待機する

VBAで基幹システム(Web)を操作するためのRPAツールを作っている。

画面に値を入力してから、内部処理が完了するまでの間に動作を待機する必要がある。一律でSleepを入れると無駄な時間が発生して動作が不安定になるのでIEの動作が完了するまで待機する処理を実装する。

うまく作れればExcelで簡単なRPAツールを作ることができる。マウス操作、クリック、指定した画面をアクティブにする、コピー、貼り付け、ぐらいは問題なく関数は作れているのであとは組み合わせるだけ。

フリーソフトをインストールする必要がないのでExcelだけ入っていれば使える簡易なRPAになる。のんびりと作っていこうと思う。


<使い方>

 ・標準モジュールに貼り付け

 ・GetIEWindowTitlesでタイトルを確認

 ・WaitIEの引数に確認したタイトルを指定(部分一致)


 Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)


'IEの動作を待機する

Public Function WaitIE(ByVal strCaption) AS Boolean

    Dim shells As Object

    Dim shell As Object

    DIm tgtIE As Object

    For i = 0 To 100

        Set shells = CreateObject("Shell.Application")

        For Each shell In shells.Windows

            If TypeName(shell.document) = "HTMLDocument" Then

                If InStr(shell.document.Title,strCaption) > 0 Then

                    Set tgtIE = shell

                    Exit For

                End If

            End IF

        Next

        If Not tgtIE Is Nothing Exit For

        Sleep 100

    Next


    If tgtIE Is Nothing Then

        WaitIE = False

    Else

        'IEが動作中の場合は待つ

        Do While tgtIE.Busy = True Or tgtIE.readyState <> 4 

            DoEvents

        Loop

        Sleep 100

        WaitIE = True

    End If


End Function


'WaitIEで指定できる画面を全て取得する

Public Function GetIEWindowTitles() As String

    Dim shells As Object

    Dim shell As Object

    Dim strValue As String

    GetIEWindowTitles = ""

    Set shells = CreateObject("Shell.Application")

    For Each shell In shells.Windows

        If TypeName(shell.document) = "HTMLDocument" Then

            If strValue <> "" Then strValue = strValue & ","

            strValue = strValue & shell.document.Title

        End IF

    Next

    Set shell = Nothing

    GetIEWindowTitles = strValue

End Function


TesseractOCRアプリケーションの作成について<手順4.OCRManager.exeを作成>

 1.NugetでOpenCvSharp4をインストールする


 2.画像を切り取る

            using (Mat mat = new Mat(strTargetFilePath))
            {
                // 画像の切り抜き
                var mat2 = mat.Clone(new Rect(intX, intY, intWidth, intHeight));
                Cv2.ImWrite(strSaveFileName, mat2);
            }

 以上。

 世の中には素晴らしいOSSを作ってくれる人がいっぱいいるんだなぁ。

TesseractOCRアプリケーションの作成について<手順3.TesseractOCR.exeを作成>

<TesseractOCRアプリケーションの画面一覧について>

  ・メイン画面・・・ジョブの状況を一目で確認できるようにする
  ・帳票定義一覧・・・帳票定義の内容を一覧で表示
  ・帳票定義詳細・・・帳票定義の詳細を更新・削除
  ・読取枠定義・・・帳票定義の読取枠の定義を更新・削除
  ・ジョブ定義一覧・・・ジョブの一覧を表示
  ・ジョブ定義詳細・・・ジョブの内容を登録・削除
  ・ジョブ結果項目修正・・・OCRの結果を確認、修正

<メイン画面>

  特に複雑な画面構成ではないので、Gridコントロールを使い、DataGridを配置していく。
  

<帳票定義一覧>

  


<帳票定義詳細>

  


<読取枠定義>

  

  この画面がTesseractOCRでは一番手間がかかると思っていたが、InkCanvasを利用すると比較的簡単に処理が記述できた。
   <画像を表示しているXaml部分>
        <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Grid.Row="0" >
            <InkCanvas Grid.Row="0" Name="ikcReadItemCanvas" Width="{Binding ActualWidth, ElementName=image}" Height="{Binding ActualHeight, ElementName=image}" 
                       HorizontalAlignment="Left" VerticalAlignment="Top" EditingMode="Select" >
                <InkCanvas.RenderTransform>
                    <MatrixTransform x:Name="matrixTransform" />
                </InkCanvas.RenderTransform>
                <InkCanvas.Background>
                    <VisualBrush >
                        <VisualBrush.Visual>
                            <Image x:Name="image" Stretch="Fill" Width="{Binding RelativeSource={RelativeSource Self}, Path=Source.PixelWidth}" Height="{Binding RelativeSource={RelativeSource Self}, Path=Source.PixelHeight}" />
                        </VisualBrush.Visual>
                    </VisualBrush>
                </InkCanvas.Background>
            </InkCanvas>
        </ScrollViewer>

   
 <呼び出し側>

        /// <summary>
        /// 画像設定処理(他の画面からこれを呼ぶ)
        /// </summary>
        /// <param name="filePath">画像ファイルパス</param>
        /// <returns>読み込み成功時true</returns>
        public bool SetImage(string filePath)
        {
            // パスが空
            if (string.IsNullOrEmpty(filePath))
            {
                return false;
            }

            var bmp = new BitmapImage();
            bmp.BeginInit();
            bmp.UriSource = new Uri(filePath);
            bmp.EndInit();
            // 画像設定
            image.Source = bmp;

            return true;
        }

  <Borderの表示方法>
     var b = new Border();
     ikcReadItemCanvas.Children.Add(b);
        InkCanvas.SetTop(b, 10);
        InkCanvas.SetLeft(b, 20);

  <保存するとき>
        y = InkCanvas.GetTop(b);
        x = InkCanvas.GetLeft(b);

  ViewModelに配列を用意しておいて、Borderをインスタンス化し、List<Border>に別管理しておいて保存時に利用するのがおすすめ。
 一応今回の画面は読取枠を表現するためにFileItemクラスというのを作成しておいて、
プロパティとしてBorderArea(Borderクラス),Title(Stringクラス)などを用意しておいた。BorderAreaプロパティは、ikcReadItemCanvasのChildrenにAddしてある。
  例)
     FileItem i = new FileItem();
         ThisViewModel.FileItems.Add(i);
      ikcReadItemCanvas.Children.Add(i.BorderArea);
         InkCanvas.SetTop(i.BorderArea, 10);
         InkCanvas.SetLeft(i.BorderArea, 20);       

<ジョブ定義一覧>

  

<ジョブ定義詳細>

  

<ジョブ結果項目修正>

  

   OCRManager.exeでOCR.exeのために画像を読取枠ごとに切り取っている。この画面はその画像を表示し、データの修正を行えるようにしている。データを修正してからダウンロードでCSVファイル化することができる。

TesseractOCRアプリケーションの作成について<手順2.WPFアプリケーションの画面作成方法(2.CsPage作成とスタイルの共通化)>

 <CsPageの作成方法について>

 プロジェクトを右クリックして、新しいファイル、ページ(WPF)を選択する。


 新しく作成したPageのxamlを修正します。

 CsWindowの時と同じで、「xmlns:controls="clr-namespace:TesseractOCR.controls"」を追加して、「<Page」を「<controls:CsPage」と修正します。

「pg01_0100_Main.xaml.cs」へ移動し、継承元のクラス名を「CsPage」へ修正するとともに「using TesseractOCR.controls;」を追加します。

 また試しに、F1キーを押下したときの処理を記述します。

 BaseWindowプロパティにインスタンスが設定されるのは、CsPageクラスがインスタンス化された後なので、PageのLoadイベント後にイベントハンドラを設定します。

※コマンドならこんな方法じゃなくていいと思いますが・・・。

<CsPageの呼び出し方>
 新しく作成したpg01_0100_Main.xamlに試しのためのTextBlockコントロールを置いて、TextBoxを配置します。

 呼び出し側)

 controls.BaseWindow window = new controls.BaseWindow(new pg01_0100_Main());

   window.Show();

 画面)


 うっすらとBaseWindowの背景色が残っています。Frameコントロールに作成したCsPageが呼び出されているのが分かります。また、BaseWindow上のボタンのContentにボタン名を設定し、Clickイベントも発生しています。

<スタイルの共通化について>
 コントロールのスタイルについてXAMLを利用するとコントロールの意味に応じたスタイルを共通で設定することができます。スタイルの継承も可能なので、下記のような形式でスタイルを一度作ってしまえば、様々なプロジェクトで利用できるのではないかと思います。

  BaseResource.xaml・・・各コントロールの基礎スタイルを定義します。

  ControlResource.xaml・・・BaseResource.xamlに定義されているスタイルを継承し、コントロールごとのスタイルを定義します。

  ProjectResource.xaml・・・ControlResource.xamlに定義されているスタイルを継承し、プロジェクトごとのスタイルを定義します。

 新しいプロジェクト(別の業務アプリを作成)するときには「BaseResource.xaml」「ControlResource.xaml」はほぼそのまま利用し、「ProjectResource.xaml」については仕様に合わせて再定義することで流用可能です。

 例)

   BaseResource.xamlに「TextBoxStyleBase」として「TextBox」のフォーカス時の背景色の設定やフォントなどを設定します。

   ControlResource.xamlに「TextBoxStyleBase」を継承して、「通常のテキストボックス(NormalTextBox)」「複数行可能なテキストボックス」のスタイルを定義します。

   ProjectResource.xamlにControlResource.xamlで定義した「通常のテキストボックス」を継承して、MaxLengthや入力文字制限を行うスタイルを定義します。


  「TextBoxStyleBase」-> 「NormalTextBox」->「txtRptIdStyle」

   ※->:は継承の意味

  こうしておくことで、業務アプリなどでよくある「得意先CD」「得意先名」といった意味のあるコントロールにすべて共通したスタイルを定義することができます。

  桁数の変更があったとしてもXAMLを修正するだけですぐに対応できます。


 <手順>

  新しいフォルダーを作成し、フォルダ名を変更します。フォルダ名:resource

  リソースディクショナリを追加します。

  追加後のフォルダ


  BaseResource.xamlについて




    今回はCsTextBoxに対してスタイルを定義しているので「xmlns:controls="clr-namespace:TesseractOCR.controls"」を記述してから、スタイルを定義しています。
 XAMLのスタイルは上に定義したスタイルを下で利用することができます。
 上図の例でいうとフォーカス時の背景色の定義である「ActiveBackColor」を一番上で定義しておくことで、「<Setter Property="Background" Value="{StaticResource ActiveBackColor}" />」で設定することができます。

   ControlResource.xamlについて
     まずは、「xmlns:controls="clr-namespace:TesseractOCR.controls"」を記述します。そのあと、先ほど定義しておいた「BaseResource.xaml」を読み込んでから継承したスタイルを定義していきます。参照方法するためには「<ResourceDictionary.MergedDictionaries>」タグを使い、読み込みたいResourceDictionaryのSourceを指定します。こうすることで、ほかのファイルに定義したスタイルを利用することができます。
     次にスタイルの継承については「BasedOn="{StaticResource LabelStyleBase}"」のように記述し、BaseResource.xamlに定義したスタイルを指定します。
     例えば先ほどの画像で定義した「TextBoxStyleBase」を継承して3つのスタイルを定義しています。「NormalTextBox」「ReadOnlyTextBox」「MultiLineTextBox」この三つのスタイルを次のProjectResource.xamlで継承し、画面コントロールごとに派生させていきます。


   ProjectResource.xamlについて
    先ほどと同じように「xmlns:controls="clr-namespace:TesseractOCR.controls"」を記述します。そのあと、先ほど定義しておいた「ControlResource.xaml」を読み込んでから継承したスタイルを定義していきます。参照方法するためには「<ResourceDictionary.MergedDictionaries>」タグを利用します。
    ここではコントロールごとのスタイルを定義します。具体的には「得意先CD」であれば最大文字数は5桁で数値入力のみ、右寄せといった具合にそのプロジェクトの中で利用する項目ごとのスタイルを定義します。基本的にはデータベースやテーブルレイアウトの定義通りになるのでスクリプトを組んで一括で作れるようにしておくと楽だと思います。
    今回の例では「帳票ID」や「JobId」がそれにあたります。数値入力のみ可能なコントロールにするには、「CsTextBox」を改修する必要がありますが、それまたの機会に記述します。
    まとめるとWPFのXAMLはCSSと同じような形で利用できるのでうまく利用しましょうということです。
      継承例)
     TextBoxStyleBase -> NormalTextBox      -> txtRptIdStyle
                                                                          -> txtJobIdStyle
              -> ReadOnlyTextBox
                                         -> MultiLineTextBox

    販売管理システムや会計管理システムそのほかの業務アプリでは同じ意味を持つ項目がいろいろな画面に置かれるので桁数のチェックや右寄せ左寄せなど本来大事にするべき業務ロジックとは関係のないところのテストや設定が必要になってしまいます。そのあたりが楽になるので、こういう機能はあるとありがたいですね。使いこなせるかというところはちょっと慣れが必要ですが、まぁ一週間毎日8時間悩んでいればいやでも身につくので頑張りましょう。










PowerShellでWPFアプリケーションをBindingするときの注意点

 1.PowerShellでWPFアプリケーションのBindingについて マニアックだけどPowerShellでWPFアプリケーションを作っている。INotifyChangedを実装したいけど、PowerShellにはgetterやsetterがないので通知が発行できない。 そ...