では、WEBで見つけたサンプルを組み合せて作った、Visual Srudioを使わない、MSCompAss用の折れ線、棒、円グラフのサンプルを載せますが、コード自体の説明はコメントを読んでいただくとして、以下はMSCompAssを使う場合(Visual Studioを使わない場合)のポイントを書きます。

 

1.MSCompAss使用の際に注意すること

Visual Studioを使うと、ユーザーが知ると否とにかかわらず、勝手に必要なコードを作ってくれるので便利である反面、WEBに載っているサンプルでは想像力を働かせて細くしなければなりません。

 

(1)先ず、サンプルでは(Visual Studio上で貼り付けたコントロールのインスタンス生成、初期設定等をInitControls()関数ですべてまとめてやっちゃうのですが、それは通常表示されていませんので)コントロールはクラスインスタンスが出来上がった状態から書かれていますので、初期設定を行わなければなりません。

 

(2)今回のMS Chartで問題となったのは、まず"using System.Windows.Forms.DataVisualization.Charting;"と書いても"using System.Windows.FormsにはDataVisualization.Chartingは見当たりません"というようなエラーが出たことです。これは↓のコードの/*~*/にあるように、csc.exeが検索するように指定されたシステムDLLが入っている場所にSystem.Windows.Forms.DataVisualization.Charting.dllがないので、コンパイラーにDLL参照指示を与える必要があるということです。ご自身のPC環境でどこにあるか調べてオプションの「DLL参照」に加える必要があります。ご参考までに末尾に私の環境におけるサンプルのoptファイルを載せておきます。

 

(3)また、Chartクラスのインスタンスを作ってグラフが表示されても「凡例」が表示されないことに気が付き、調べた結果、凡例を表示するにはCharクラスインスタンスのLegendプロパティにAdd()メソッドを使って凡例を作ってやることが必要であることが分かりました。"chart.Legends.Add("Legend1");"

 

2.MS Chartの使い方

C#でMS Chartを使うのは極めて簡単です。いろいろなサンプルで記述が一様ではないのですが、私は以下のように統一的な書き方にしてみました。

 

(1)まず参照できる範囲にChartクラスインスタンスを作成する

C#では"Chart chart;"とクラスインスタンスを宣言してもその段階では必要なメモリー領域が確保できていないのでエラーになります。(エラーにはなりませんが、C++の"Chart* chart;"なんだ、と理解するとよいと思います。)したがって可視範囲にクラスインスタンスを宣言したらその段階で"new Chart();"としてインスタンスを確保するか、↓のコードのようにコンストラクターやWM_CREATEの段階で確保する必要があります。

なお、C++では"new したら、delete"ですが、C#ではマネージされたリソースは"dispose"メソッドで開放する必要がありませんが、マネージされていないリソースを使うとユーザーでdisopseによる開放を行う必要があります。Chartのサンプルでは特印disopse()していなかったのでマネージされたリソースになるのかな、と思っていましたが、Microsoft Docsではdispose(bool)メソッドがあり、「trueはマネージされたリソース、マネージされていないリソースの両方、falseはマネージされていないリソースのみ開放」と書かれています。まぁ、サンプルでも使っていなかったのでマニュアル開放はしていませんが、気になる方はdispose()してください。(ここがC#のやりにくいところなんですが...)

 

(2)(凡例を使いたい場合)作ったChartインスタンスに凡例(Legend)を追加(Add)する

上記↑1.(3)参照。

 

(3)MS Chartの初期化

最初のまっさらなChartインスタンスでは問題ないのですが、一度使ったインスタンスは再初期化する必要があります。一般的な初期化対象はChartインスタンスの(以下に述べる)「領域」、その「名称」(タイトル)と「系列」になります。

 

(4)(タイトルが必要であれば)タイトル設定

次に述べるグラフを描く「領域」にタイトルが設定できます。

 

(5)チャートエリア作成(複数可)

MS Chartは丁度「グラフを描くキャンバス」のようなものだと考えてください。このキャンバスは最初一つですが、複数のグラフ領域(チャートエリア-ChartArea )に分割することもでき、それぞれに個別のタイトルを与えることが出来ます。

 

(6)系列の作成(複数可)

チャートエリアが決まったら、それぞれに描くグラプとなる「系列-Series」を作り、グラフ種類を決めてそれぞれのグラフ種類に合わせた設定を行ってゆきます。

一つのチャートエリアに複数のグラフを書くことも可能(Sample 2)ですし、チャートエリアを分けて複数のグラフを書くことも可能(Sample 4)です。

 

(7)(グラフの)データ設定

データは系列のPoints配列(コレクション)に追加(Addメソッド)してゆけばよく、チャートに凡例(Legend)を作成しおればそれに反映されます。

 

(8)チャート設定

最後にChartインスタンスに名前、領域、系列をAdd()メソッドで設定します。また個別のグラフについて背景色等、Chart自体の設定を行うこともできます。

 

3.MS Chartのサンプルについて

以下のコードは、MS Chartの上でマウスクリックを行うと、

 

(1)単純な折れ線グラフ(Sample 1

(2)折れ線グラフと棒グラフの混合(Sample 2

(3)円グラフ(Sample 3

(4)領域を区分した折れ線グラフと棒グラフの混合(Sample 4

 

を表示するサンプルです。

 

さて、いかがでしたか?C++プログラミングでは考えられないような簡単さで、矢張りVisual Basicを髣髴とさせます。しかし、如何にMS Chartが簡単に使えても、家庭でグラフを見て分析するような問題は余り考えられず、これならExcelでやった方が簡単、ということも事実ですね!同様にDataGridも使えますが、これも矢張り完成されているExcelの方が簡単なので余り食指が動きません。「【.NET】C# や VB で使う全コントロール一覧(サンプル画像付き)」というサイトも見ましたが、見慣れたコントロールばかりですね。また、何か面白いネタを考えてみましょう。

 

【SampleMSChart.cs】

//Visual Studioで行われる一般的Windowsプログラム用クラス宣言
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;
//MS Chart利用の為追加される宣言
using System.Windows.Forms.DataVisualization.Charting;
/*
    但し、DataVisualizationクラスは一般的なクラスが入っているフォールダー
        C:\Windows\Microsoft.NET\assembly\GAC_32(64)
    ではなく、csc.exeがデフォルトでは参照しないフォールダーである
        C:\Windows\Microsoft.NET\assembly\GAC_MSIL
    に入っているため、Visual Studioではプロジェクトの「参照」で行うcsc.exe
    のオプションに以下を与える必要がある。
    "RefFile=C:\Windows\Microsoft.NET\assembly\GAC_MSIL\...\System.Windows.Forms.DataVisualization.dll"
    (注:私のシステムで...は"System.Windows.Forms.DataVisualization\v4.0_4.0.0.0__31bf3856ad364e35")
*/


namespace SampleMSChart
{
    public partial class SampleChart : Form
    {
        //MS Charコントロールインスタンス(ポインター)
        Chart chart;

        //エントリーポイント
        [STAThread]
        public static void Main()
        {
            Application.Run(new SampleChart());
        }

        //コンストラクター
        public SampleChart()
        {
            //フォームの設定
            this.Size = new Size(800, 480);
            this.MinimumSize = new Size(320, 190);
            this.Text = "Sample - MS Chart";
            this.Load += Form_Load;        //WM_CREATE処理
        }

        //WM_CREATE処理
        private void Form_Load(object sender, EventArgs e)
        {
            //Visual Studioでは以下の処理が InitControls(); で行われる
            //MS Chart インスタンスの生成と設定

            chart = new Chart();
            chart.Location = new Point(10, 10);
            chart.Size = new Size(ClientSize.Width - 20, ClientSize.Height - 20);
            chart.Anchor = (AnchorStyles.Top | AnchorStyles.Right | AnchorStyles.Bottom | AnchorStyles.Left);
            chart.Legends.Add("Legend1");    //Addしないと「凡例」が表示されない
/*
            //凡例の幅と高さを設定
            chart.Legends[0].Position.Auto = false;
            chart.Legends[0].Position.Width = 20.0f;
            chart.Legends[0].Position.Height = 20.0f;
            //凡例の位置を設定します
            chart.Legends[0].Position.X = 80.0f; 
            chart.Legends[0].Position.Y = 90.0f;
*/

            this.Controls.Add(chart);
            //MS Chart上でマウスクリックした場合の処理を追加
            chart.Click += Chart_Click;
        }

        //MS Chart上でマウスクリックした場合の処理
        private void Chart_Click(object sender, System.EventArgs e)
        {
            //Sample 1
            MessageBox.Show("最初のサンプルです", "サンプル1", MessageBoxButtons.OK, MessageBoxIcon.Information);
            Sample1();
            //Sample 2
            MessageBox.Show("二番目ののサンプルです", "サンプル2", MessageBoxButtons.OK, MessageBoxIcon.Information);
            Sample2();
            //Sample 3
            MessageBox.Show("三番目のサンプルです", "サンプル3", MessageBoxButtons.OK, MessageBoxIcon.Information);
            Sample3();
            //Sample 4
            MessageBox.Show("最後のサンプルです", "サンプル4", MessageBoxButtons.OK, MessageBoxIcon.Information);
            Sample4();
        }
        
        //Sample 1
        private void Sample1()
        {
            //サンプル用データ 
            double[] pointX = { 1.0, 2.0, 3.0, 4.0, 5.0 };
            double[] pointY = { 1.0, 2.0, 3.0, 4.0, 5.0 };
            //MS Chartの初期化
            chart.Series.Clear();
            chart.ChartAreas.Clear();
            chart.Titles.Clear();
            //タイトル作成
            Title title = new Title("サンプル1");
            //チャートエリアの作成
            ChartArea chartAria = new ChartArea("Area1");
            //系列(Series)の作成
            Series series = new Series();
            series.ChartType = SeriesChartType.Line;    //系列の種類を折れ線グラフ(Line)に設定
            series.LegendText = "凡例";                    //系列の凡例を設置
            series.IsVisibleInLegend = true;
            //ポイントデータを設定
            for (int i = 0; i < 5; i++)
            {
                series.Points.AddXY(pointX[i], pointY[i]);
            }
            //チャートの設定
            chart.Titles.Add(title);
            chart.ChartAreas.Add(chartAria);
            chart.Series.Add(series);
        }
        
        //Sample 2
        private void Sample2()
        {
            //MS Chartの初期化
            chart.Series.Clear();
            chart.ChartAreas.Clear();
            chart.Titles.Clear();
            //タイトル設定
            Title title = new Title("サンプル2");
            //チャートエリアの作成
            ChartArea chartAria = new ChartArea("Area1");
            chartAria.AxisX.Title = "X軸";
            chartAria.AxisY.Title = "Y軸";
            //系列の作成
            Series series = new Series();
            series.ChartType = SeriesChartType.Line;    //系列の種類を折れ線グラフ(Line)に設定
            series.LegendText = "凡例:折れ線グラフ";    //系列の凡例を設置
            series.BorderWidth = 3;                        //系列の境界線太さ
            series.MarkerStyle = MarkerStyle.Circle;    //系列のポイントマークの形状
            series.MarkerSize = 12;                        //系列のポイントマークの大きさ
            series.Color = Color.BlueViolet;
            //系列の追加とデータ作成
            Series seriesColumn = new Series();
            seriesColumn.LegendText = "凡例:棒グラフ";
            seriesColumn.ChartType = SeriesChartType.Column;
            seriesColumn.Color = Color.DeepSkyBlue;
            //乱数を使ったデータ作成
            Random rdm = new Random();
            for (int i = 0; i < 10; i++)
            {
                series.Points.Add(new DataPoint(i, rdm.Next(0, 210)));
            }
            for (int i = 0; i < 10; i++)
            {
                seriesColumn.Points.Add(new DataPoint(i, rdm.Next(0, 210)));
            }
            //チャート設定
            chart.Titles.Add(title);
            chart.ChartAreas.Add(chartAria);
            chart.Series.Add(seriesColumn);
            chart.Series.Add(series);
        }

        //Sample 3
        private void Sample3()
        {
            //MS Chartの初期化
            chart.Series.Clear();
            chart.ChartAreas.Clear();
            chart.Titles.Clear();
            //タイトル設定
            Title title = new Title("サンプル3");
            //チャートエリア作成
            ChartArea chartAria = new ChartArea("Area1");
            //系列の作成
            string legend = "PieGraph";
            Series series = new Series(legend);
//            series.ChartType = SeriesChartType.Pie;            //系列の種類を円グラフ(Pie)に設定
            series.ChartType = SeriesChartType.Doughnut;    //系列の種類を円グラフ(Doughnut)に設定
            series.LegendText = "凡例:円グラフ";            //系列の凡例を設置
            series.BorderWidth = 2;                            //系列の境界線太さ
            //乱数を使ったデータ作成
            Random rdm = new Random();
            double[] values = new double[5];
            double total = 0.0;
            for(int i = 0; i < values.Length; i++)
            {
                values[i] = (double)rdm.Next(0, 20);
                total += values[i];
            }
            //系列にデータを設定
            for(int i = 0; i < values.Length; i++)
            {
                double rate = (int)(values[i]  * 10000.0 / total) / 100.0;    //小数点以下2桁にする
                series.Points.Add(new DataPoint(rate, rate));
                series.Points[i].LegendText = "製品" + i.ToString() + ": " + rate.ToString(); 
            }
            //チャート設定
            chart.Titles.Add(title);
            chart.ChartAreas.Add(chartAria);
            chart.Series.Add(series);
            chart.ChartAreas["Area1"].BackColor = Color.SeaShell;    //背景色を変更
        }

        //Sample 4
        private void Sample4()
        {
            //MS Chartの初期化
            chart.Series.Clear();
            chart.ChartAreas.Clear();
            chart.Titles.Clear();
            //タイトル設定
            Title title = new Title("サンプル4");
            Title title1 = new Title("グラフ1");
            Title title2 = new Title("グラフ2");
            title1.DockedToChartArea = "Area1";        // ChartAreaとの紐付
            title2.DockedToChartArea = "Area2";        // ChartAreaとの紐付
            //チャートエリア作成
            ChartArea chartAria = new ChartArea("Area1");
            chartAria.AxisX.Title = "X軸";
            chartAria.AxisY.Title = "Y軸";
            ChartArea chartAria2 = new ChartArea("Area2");
            chartAria2.AxisX.Title = "X軸";
            chartAria2.AxisY.Title = "Y軸";
            //系列の作成
            Series series = new Series();
            series.ChartType = SeriesChartType.Line;    //系列の種類を折れ線グラフ(Line)に設定
            series.LegendText = "凡例:折れ線グラフ";    //系列の凡例を設置
            series.BorderWidth = 2;                        //系列の境界線太さ
            series.MarkerStyle = MarkerStyle.Circle;    //系列のポイントマークの形状
            series.MarkerSize = 12;                        //系列のポイントマークの大きさ
             //乱数を使ったデータ作成
            Random rdm = new Random();
            for (int i = 0; i < 10; i++)
            {
                series.Points.Add(new DataPoint(i, rdm.Next(0, 210)));
            }
            series.ChartArea = "Area1";                    // ChartAreaとの紐付
            //系列の追加とデータ作成
            Series seriesColumn = new Series();
            seriesColumn.LegendText = "凡例:棒グラフ";
            seriesColumn.ChartType = SeriesChartType.Column;
            for (int i = 0; i < 10; i++)
            {
                seriesColumn.Points.Add(new DataPoint(i, rdm.Next(0, 210)));
            }
            seriesColumn.ChartArea = "Area2";            // ChartAreaとの紐付
            //チャート設定
            chart.Titles.Add(title);
            chart.Titles.Add(title1);
            chart.Titles.Add(title2);
            chart.ChartAreas.Add(chartAria);
            chart.ChartAreas.Add(chartAria2);
            chart.Series.Add(series);
            chart.Series.Add(seriesColumn);
            chart.ChartAreas["Area1"].BackColor = Color.WhiteSmoke;    //背景色を変更
        }
    }
}

 

【SampleMSChart.opt】

[Compile Option]
Target=2
Resource=0
RscFile=
IconFile=C:\Users\ysama\Programing\C# Programing\Samples\Resources\resources\CatFace.ico
DbgOpt=0
WarnErr=5
RefFile=C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Windows.Forms.DataVisualization\v4.0_4.0.0.0__31bf3856ad364e35\System.Windows.Forms.DataVisualization.dll
Others=
 

前回C#によるMS Chart利用の話で「フリーの32bitベースのEmbarcadero C++の生き延びられる時間も刻々と短くなってきているのではないでしょうか?」と書いて、またこの「暇人プログラミング趣味20余年」を振り返りたくなりました。

 

私のプログラミング歴はすでに書きました。

 

 

 

 

そして今は64bitのRyzen 7+Windows 11でプログラミングしていますが、私が初めてCに触れたのが31歳、C++に触れたのがニューヨーク駐在時代の36歳、そして自己学習の為にBCCForm and BCCSkelton等自分の開発環境を作り、公表したのがシンガポール駐在時代の46歳だったので、今から22年前のことになります。この20年間の違いは感慨深く、思いつくままに書き留めておきたくなったのがその理由です。

 

1.ハードウェア

(1)CPU

調べてみると20年前のIntel v. AMDは"Pentium 4 v. Athlon XP"だったみたいですね。勿論まだ32bitのシングルコアなのかな?当時は「クロック戦争」だった気がします。もちろん当時もRISCはありましたが、「ビジネス→Office→Windows→Microsoft」軍団がIntel(およびAMD)とハネムーンだったので、「学術研究→→Apple」(及びインターネット系企業)サイドに居りました。注:抜粋

ARMは,1990年11月,Apple Computer,Acorn Computer Group,VLSI Technologyの3社による英国拠点の合弁事業『Advanced RISC Machines Ltd.』として設立されました.AppleとVLSIの両社は資金を提供し,Acornは技術とARMの設立に貢献した12人のエンジニアを提供しました.世界初の商業用シングルチップRISCプロセッサを開発したAcornと,自社システムにRISC技術の導入を進めようとしていたAppleは,ARMによって新しいマイクロプロセッサ標準を作りあげようとしました.ARMは,デスクトップコンピューティングに理想的な初の低価格RISCアーキテクチャを作ることによって,市場での差別化に成功しました.逆に,性能の向上に焦点を当てることの多かった競合アーキテクチャは,まずハイエンドワークステーションに採用されました.

 1991年,初の組み込み型RISCコアであるARM6を発表すると,ARMは最初のライセンシとしてVLSIと契約しました.1年後,シャープ(株)とGEC Plesseyがライセンシに加わり,1993年にはTexas InstrumentsとCirrus Logicが続きました.

1995年にはARMの特徴である,16ビットのシステムコストで32ビットRISCのパフォーマンスの提供を可能にするThumbアーキテクチャ拡張を発表しました.1998年にはARM7TDMIのシンセサイザブルバージョンを,1999年には信号処理の拡張されたシンセサイザブルなARM9Eプロセッサ,2000年にはJavaアプリケーション向けに拡張されたアーキテクチャJazelleテクノロジを発表しました.

 このようにARMは,数年の間に,アーキテクチャや製品ファミリを拡充し,ライセンシを増やすだけでなく,Bluetoothなどの先進技術へもいち早く対応してきました.さらにARMは,サードパーティとも密接に連携して開発環境を整え,パートナーシップを拡大し,より多くのエンジニアに,ARM技術へのアクセスするための選択肢を多数提供しています.

その時代から考えると、現在の64bit、3GH以上の複数コアCPUや、RISCベースのWindows、スマホ由来のOS等隔世の感(速度も!)があります。

 

(2)外部記憶装置等

2002年当時は勿論PCの外部記憶装置はHDが主流で、確かNYから帰ってきた1996年当時はまだ「数100MBで1GB未満」、2002年当時は「数10GBで100GB未満」だったように記憶(注)しています。それが今ではHDは遅くて駆逐されてSSDが主流となり、容量も気が遠くなるほど大きなものとなっています。昔の8bit時代の「64KBの広大なメモリー空間」という言葉が懐かしいです。

注:正しかったようです。ご参考。CPUとメモリー、HDは飛躍的に進化していますが、ディスプレーは漸く「薄型(LCD)」になってきたということか、SVGA程度のままですね。こんなのあんなのの記事も当時をしのぶ上で参考になります。

 

2.OS

(1)Windows

今は昔、学生ベンチャーのビルゲイツがAltair Basicで一儲けし、更にSCPからライセンスを買い取って開発したMS-DOSでIBMと付き合うようになり、別れる際に共同開発したOS2を捨ててWindowsに集中し、インターネットの普及を背景にWindows 95で大成功をおさめ帝国を築いたことはよく知られる話です。

私は最初の駐在地米国でPC互換機上の16bit Windows 3.xから(日本の発売よりも1年前から)32bit Windows 95を経験し、20年前のシンガポールではWindows 98~XPを使っていましたが、現在は32bit Windows 10と64bit Windows 11を併用しています。このようにウィンドウズの歴史(画面が懐かしい!スプラッシュ画面の推移も!)は35年にもなるようです。

しかし、プログラムミング面から受ける印象では、20年前にBCCForm and BCCSkelton等自分の開発環境を作っていた時のベースはWin32 APIという関数群を呼び出すことが基本であり、現在のCRLに基づく様々なウィンドウコンポーネントオブジェクトをウィンドウ(フォーム)に張り付けるようなObject化に比べると、矢張り「構造化された手続き型処理」的な印象をぬぐい切れません。

 

(2)その他OS

ローランドではないですが、20年以上前のPC環境は「Windowsか、それ以外か」のような状況で、対抗馬はMac OS(といってもPCは全く別物なので「Windows v. Macの商品戦」ですね)、(Intelチップで動くOSとしては)穴馬がLinuxかなって感じでした。(OS2 WarpなんてWindows 95にやられて直ぐにご臨終だったし。)

この為、当時のMicrosoftは(現在は信じられませんが)「OSや開発環境を高額で販売する独占(がビジネスモデルの)企業」であり、NTサーバー用OSやVisual Studio(「Visual Studioの歴史」的な記事)を購入すれば数十万円!、(Visual Basic、Visual C++isual単体でも数万円!)というご商売を続けておられました。

しかし、まったく異なる切り口(スマホやパッド等)からの攻撃により、PC自体がコンピューティングの中心から外れてゆき、「Microsoft帝国」のマーケットシェアもじわじわと低下し始めます。

Windowsのシェアが初の90%割れ、IEも70%切る-米Net Applications調べ(2008年12月03日記事)
【悲報】Windowsのマーケットシェアが過去10年間で大幅に減少していた(2022年04月27日記事)

そしてとうとうWindowsは10や11(現在1万円位、Proでも3万円弱)から「OSアップグレード商売」をおやめになって無償アップグレードとし、Visual StudioもCOmmunity版はロハになるという現状となりました。が、依然事務所や家庭で使用するPCのOSはWindowsが圧倒的に多いというのも事実です。現在も市販されるIntel/ADMチップマシンで動くその他のOSは選択肢があまりなく、基本Linux系になると思います(また使い方にもよります)が、プログラミングを趣味にするならフリー開発環境の豊富で昔ほど阿漕なOSでなくなった64bit Windowsは毛嫌いするほどのものではないかと考えます。

ご参考:「PCのOS種類」的な記事

 

3.ネットワーク

(1)インターネット

昔は「パソコン通信(因みに私は経済的理由でやらなかった)」などというものもありましたが、インターネットが矢張り社会的に認知され、商用化されたのは1990年半ばからではないかと思います。(私も初めてインターネットに接続したのは米国でAOLにつないだときでしたが、何せ電話線のインターネットで画面遷移が超遅く、電話代ばかりかさむのでまず使いませんでしたね。)

30年前のニューヨーク駐在時代はWindows 3.1 for WorkGroupを使って現地法人事務所内にLANを作ったりしましたが、20年前では(セキュリティはまだ低かったものの...汗;)企業の社内LANも整備されていました。現在もネットワークといえば「会社のLAN」か「家庭のインターネット」ですが、ネットワークセキュリティが強化された現在会社LANで遊ぶことなど考えられなくなりましたので、「インターネット一択」じゃないでしょうか?

 

(2)ADSL

インターネットが遅かった昔でも、ネットに接続できるのは有難く、速度も改善されてきましたが、Windows Update等を電話線で行うのは地獄でした。この状況が劇的に改善されたのは、シンガポール駐在当時利用したSingtelADSLからです。しかし、ご存じの通り、今や絶滅確定です。(NTT系は2023年1月31日までに順次終了を発表、ソフトバンクも2024年3月末で終了を発表。[

 

(3)光ケーブルやWi-Fi

ということで、20年前にはなかった光ケーブルやWi-Fiを現在は当たり前に利用し、高速のネットワーク接続をエンジョイしている現状にあります。

 

4.統合開発環境

さて、いよいよ本題のプログラミングについてですが、プログラミングはコーディング、コンパイル、ビルド、デバッグ等の処理を行うわけですが、20年前でも個人で統合開発環境(IDE-Integrated Development Environment)を持つことは、お金さえあれば(注)、可能でした。

 

(1)Microsoft

その横綱がMicrosoftの開発ソフト群でしょう。しかし、何しろ

値段が高い!」(注)

私も2004年にVisual C++とVisual Basicを買った経験があります。(購入の際は、息子が買うことにして連れてゆき学割をゲット!それでも4万弱くらいかかったと記憶しています→にも拘らずVisual C++は(WYSWYGではない)MFCが使い勝手が悪く、Visual Basicは簡単なのですが「ネタがなく」、結果的に何も作りませんでしたが...泣;)

注:2002年頃のデータを探したのですが無く、8年後の2010年のデータがありましたので参考までに。8年後でこれですから、当時の値段は推して知るべし。

では、今はどうか?「値段が高い」というのも昔の話で、現在はCommunity Editionならフリーで手に入るし、言語もC++、C#、Visual Basic、Java、Paython等なんでもござれですし、その他にもSmall Basicや(もぅ使うな、とされている)WSHなどもあり、Windows上でウィンドウベースで何か作ろうと思えば簡単に良いツールがフリーで手に入る時代になりました。

 

(2)フリーソフト

前にも書きましたが、MS-DOSの逐次処理型プログラミングからWindowsの割り込み型プログラミングへの蒙を開いてもらったのはActive Basicでした。しかしこのフリーソフトも時代と技術の進展(および大資本の集約化)に追いついていけずに今は跡形もないですね。

一時、DLして調べてみたのがWide StudioというC++ベースの、コントロールから何から総て手作りのクロスプラットフォームウィンドウズ(OSのWindowsベースではなく、矢張りオリジナルのようでしたが)プログラミング統合環境です。

すごいっ!!!

のですが、しかし、手作りのせいかWindowsベースは処理速度が受忍限度を超えて遅かったので使いませんでした。

では、今はどうか?昔よく覗きに行ったVectorの「プログラミング」の「C言語」、のみならず他の言語についても閑古鳥が鳴いています。現在の充実したフリー開発環境や、分からないことは総てWEB上で調べられるプログラミング関連記事の豊富さ、加えてGitHub上のシェアコード、ツールやユーティリティなどもフリーなので、何も不自由なくなんでも手に入れられる環境になりました。

 

(3)Borland

そんなこんなでウェブをウロウロしていたら見つけたのが、(旧)Borland社のコマンドラインベースのC++コンパイラー(bcc55)。(コンソールベースでないと使えないTurbo Debuggerもついて)

フリー

というのが男前でした。ということで早速ダウンロード。

では、今はどうか?すでにBorland、承継したImpressもなく、EmbarcaderoがDelphi(含Linux版のKylix)やBorland C++ Builder(BCB)等を提供して営業していますが、自らOS,Windowsを抱えるMicrosoftがWYSWYGベースの統合開発環境をフリーにしてしまったので苦戦を強いられているのが現状ではないでしょうか?

 

(4)その他

やっとこさフリーのC++コンパイラーを入手しましたが、bcc55はそのままでは矢張りコンソールに"Hello world!"を表示するプログラムくらいしかできないのでフリーのツールを探していたら、Delphiで書かれたエディター兼ビルダーで、フリーのbccを使って統合環境が構築できる、といううたい文句のBCC Developerというツールを見つけました。そして日本語版bcc55+BCC Developerを核として使いながら、他にもリソースエディター等のツール付き英語版bcc55や、Borland C++ Builder 5がシンガポールのチャンギ空港やマレーシア空港などにワゴンセールで売られていることがあり、参考までに購入したことを覚えています。

では、今はどうか?2000年の発表当時はbcc55用の開発環境を提供する様々なフリーツールがアップされていましたが、現在は後継の矢張りフリーのbcc102用のツールは無く、私の知る限り関連サイトもアクティブなものはここ以外に一つしかないのではないでしょうか?

 

5.私の成果物

結局20年前は、Active Basicやbcc55を手に入れ、(数十万円も出せないので)フリーの開発ツールを使ってコツコツと自分なりの開発環境を整備してきた、といえるかもしれません。

 

(1)ライブラリー

実際、20年前に何を作ったかというと95%が開発関連ツールだけです。まぁ、学習しながら習作として作り始めたBCCSkeltonですが、それを更に使って開発ツールを作っていったということですね。

 

(2)アプリケーション

では、今はどうか?というと、BCCSkeltonのUTF-16対応ライブラリーであるECCSkelton、開発ツールとしてBatchGoodやMSCompAss(EZImageやIconViewer等のリニューアルを含む)等もやりましたが、実用的なIDList、DirectShow、Album、FileHandler、RTWEditor、TextSpeech、Renamer、Resizer、Replacer等も「サンプルプログラム」として開発できました。

 

(3)ゲーム

更にLifeGame、LightCycle、Dice、MENACE、NimやTower of Hanoi等のゲームも開発することができました。

 

(4)そして、結論めいたもの...

しかし、今回C#でMSChartのサンプルを作って思ったのは(Microsoftが昔の阿漕な商売をやめ、フリーで充実した開発環境を提供するようになったので)「フリーの32bitベースのEmbarcadero C++の生き延びられる時間も刻々と短くなってきているのではないでしょうか?」ということで、その理由は以下のようなことです。

 

①もう暇人プログラマーが自分で開発環境やツールを作る必要や必然性がなくなってしまったこと

②Cの進化系であるC++も今や「アッセンブラー的低級言語」となり、ウィンドウズの(みならずクロスプラットフォームで他のシステムにも移植される)コンポーネントオブジェクトを貼り付けてプロパティを設定したり、イベント処理をするだけで簡単に(コーディング量が削減されたWYSWYGによる)ウィンドウズプログラミングが可能になったこと

③またその成果物が更にコンポーネントとしてOSの資産となり、再利用されること

④人間が事務所、家庭で必要とする処理を行うプログラムがほぼ全般フリーで入手可能になり、「自分でプログラムを組む」必要性や必然性が激減したこと

 

実際Edgeをはじめ、Word、Excel、PowerPoint、Access、Mailer、SchedulerであるOutlook等があれば、何も作る必要がないんです。如何にMS ChartがC#で使えるようになっても、わざわざソフトを作るよりもExcelでグラフを作った方が早いんです。

 

(5)そして、質問

では、(20年前は想像だにできなかった)あれだけ充実した開発環境であるVisual Studioを手に入れて人は何を作っているのだろう?

コンソール版の"Hello , world!"表示プログラム?

まさかね。

 

ps. なんでもPCを使ってウェブで調べて、自分が覚える必要がなくなり、最近は漢字も書けなくなってきて、暗算も弱くなり、ゲームやSNSばかりに腐心してしまう自分を顧みると、

AIのSingularityよりも、人類の痴呆化の方がより重大な脅威

のように思えてきました。

プログラミングネタが思いつかないので、アイデア出しの間に目についたこと、思いついたことなどをつらつらと書いてみます。

 

MS Chartという汎用コントロールがあるということを初めて知った時に、これをフリーのEmbarcadero C++で何とか使えないか、と考えました。そしてウェブで調べまくっているうちにこのページにたどり着き、ほぼ絶望的であることを知り、がっかりしました。

 

何故そのように結論付けたかというと、このページに書かれているように、

 

"Using the MFC Windows Forms support classes, you can host Windows Forms controls within your MFC applications as an ActiveX control within MFC dialog boxes or views. In addition, Windows Forms forms can be hosted as MFC dialog boxes."

 

MS ChartをMFC(Microsoft Foundation Class)で使う可能性について書かれているのであり、ATLもVCLもないフリーのEmbarcadero C++では、まず使えないからです。(注)

注:このページによると、昔はBorlandもライセンスを持ってActiveXコントロールを使えるようにしていた(Active Template LibraryーATLを提供していた)のですが、MicrosoftがEmbarcaderoにライセンス提供を断ったために(またActiveXが技術的にも陳腐化してきたこともあるそうです)ActiveXコントロールを自らのコード(Delphi ActiveveX-DAX)に切り替えました。このライブラリーはVCL等DelphiやBCBのライブラリーに含まれていると思います。

 

同時に、(当時はプログラミング経験もなく全く無知であった)C#では至極簡単に利用できるということを知り、随分うらやましかったことを覚えています。(一応C#を含むVisual Studioをダウンロードしていたのですが、当時の私の環境では余りに重く、エディターを使うだけで電源を切りたくなりました!)

 

そんなことを突然思い出し、ウェブに乗っているサンプルを見て、(Visual Studio無しの)MSCompAssでプログラミングしてみることにしました。

 

私のプログラミング学習のポイントは、サンプルコードもそのままコピー、ペーストするだけではなく、改造し、コメントをつけて自分なりに消化してみることです。実際、色々と改造してゆく中で壁にぶつかったり、不思議減少に出会ったりして更に知見を深めてゆくことができます。そして、今日遊んだ成果が↓です。

 

なんて簡単なんでしょう?!?

Visual StudioをDLしなくても、ある程度のグラフを使用したプログラムならMSCompAssで組めそうですね。(それがWindowsを作ったMicrosoft親分のC#の大きな利点ですが。)

 

次回はコードとともに雑感(注)なども書いてみましょうか?

 

注:.NetFrameや新しい.NETのコントロールなどは、もう旧いC++でカバーできなくなってきています。CLR(Common Language Runtime)を使うことを前提としたC++/CLI(. NET Frameworkの共通言語基盤 (CLI) 上で実行するプログラムを作るためにC++を拡張したプログラミング言語)なども出てきており、フリーの32bitベースのEmbarcader C++の生き延びられる時間も刻々と短くなってきているのではないでしょうか?

 

娘と孫が来ていたので、ブログ更新はお休みしていました。

が、

更新しようにも、次のネタがないので、出来ないのです。

 

C++ + ECCSkeltonのRenamerは、カミさんの写真ファイルがドバっと未整理であったので、二日かけて整理しましたが、その際に使うツールでした。また、C#のSDI Skeltonで作ったReplacerはRenamerを使う際に出される「旧ファイルパス・名,新ファイルパス・名」というCSVファイルを使った全文検索・置換ツールで、「必要性に裏打ちされた開発理由=大義」がありました。

しかし、今は

そのような大義がありません。

なので、

雌伏するのみ

と心に決めて、孫をあやそうと思っています。

 

フ~ン、そうか。幼小児用のソフト

 

という手があるかもしれない。しかし、孫はまだ2歳になっていませんが、母親のスマホをいたずらして私にTV電話をかけてくるようなツワモノ、昔の子供だましでは通用しないかもしれませんね。

 

本日何気なくReplacerを起動したら、

ウィンドウタイトルの

 

「文字列一括返還ユーティリティ」

 

を見て逆上!(「目」が・)

 

ヒッ、ヒドイッ!

 

北方領土じゃないんだから、もう。

 

ソースを直して再コンパイル。修正版は次のアップデート時に載せます。

 

ps. 大体BCB5で書いた元のプログラムは「文字列一括置換ユーティリティ」じゃないか、って?すんません。勘弁つかぁさい。

 

今年の確定申告を本日終えて、今日はちょっと(辛口の)個人的意見を書かせていただきます。

 

(1)e-Taxアカウント

私は確定申告の電子ファイリングができる、ということですぐにアカウントを開きましたが、「電子ファイリングができる」にはマイナンバーカードを取得して、カードリーダーを有償で買って使わないと分かり、ID(利用者識別番号)とパスワードでのe-Tax申告にしました。(税申告コストを国民に負わせる、のはまだわかるとして、その為に任意のマイナンバーカードを取得させるのみならず、そのリーダー<セキュリティはどうなっているの?>迄かわせるってどーよ?)

現在もe-Taxアカウントはあり、「e-Taxソフト(WEB版)」でログインはできますが、唯一利用可能な「(税)申告データの確認」はマイナンバーカードリーダーがないので読めません。(スマートフォンだとマイナンバーカードのQRコード?を読むらしいですが、マイナカード、ねえし。注)ということで、使えない「つかえねぇ」状態です。

注:カードリーダーがないのでe=Taxファイリングが普及しない原因となったためか、2022年からカードリーダーの代わりにQRコードで申告登録ができるようになったみたいですね。しかし、その内容等はわかりづらいのでほとんどの方が知らないのではないかしら?いずれにしてもマイナカードは必要です。税庁からのお知らせ <パソコンとスマホでe-Tax!マイナンバーカードをスマホで読み取り>:令和3年分 確定申告特集 (nta.go.jp)

これは別システムの「受付ソフト」という奴でログインしても変わらず、「メッセージボックス一覧」という奴の中にある申告データを参照しようとしても、カードリーダーまたは「電子証明書(んなもの、持ってねぇし)」がなければ参照できないので、矢張り使えない「つかえねぇ」状態です。また、「マイページ」というのがあるので見てみると、単に私の個人登録情報が参照できるだけです。つまり、何の役にも立ちません。

結論として、お上が国民の情報を取得し、国民の労力とコストで税申告をさせるだけの目的で、国民の利便は全く考えていないソフトであるといえますね。

 

(2)確定申告作成コーナーソフト

これは(マイナンバーカードでe-Tax申告を促進する意図が叶わず、相変わらず従来のID、パスワードでも国税の労力がかからないように作った)e-Taxとは全く別物の「ツール」ソフトです。

これの為に税務署はリタイアされた方を含めて動員での説明会場対応におうわらわですね。(その結果、職員の方の愛想の程度はおのずと知れますね。)こういう集中型の業務の常で、動員された方々のコミュニケーション能力や知見レベルはまちまちで、(まぁ、訳の分かんないじーさん、ばーさん相手で疲れ果てて笑えってものが無理だ、ということはわかりますが)必ずしも適切なアドバイスが、適切に伝達されているか否かは確言はできない所でしょう。

このブログの趣旨からいって、「人」はさておき、「ソフト」について言及すれば、

①ID、パスワードでログインして(実際にはログインではなく、税申告上の利用者識別番号の採取のためかもしれませんが)入力を始めると、収入・所得の入力、各種控除の入力、税情報の入力を行って、最後にマイナンバー(作成された税申告書の上にでかでかと明示されます)を入力してから、税務署に送信(実際には登録処理を)して完了します。

②「長く、税に不慣れな方の入力処理」なので、途中で間違ったり、中断することが予定されると思うのですが、作成された時代からか、最近のブログのような「自動的に入力内容を保存」してくれるような機能はありません。従って作成者がマニュアルで作成途上のデータを「保存」しなければならないのですが、(これが結構トリックで、実際には)「ローカル端末(PCやスマホ)」の記憶装置に「ダウンロード」する形(ファイル名は"rXsyotoku.data")で保存します。つまり、「e-Taxアカウントと連動してもいませんし、この『確定申告作成コーナー』ソフトのサーバーにも全く何も残らない」ので、「次にID、パスワードで確定申告作成コーナーソフトにログインしても、データをダウンロードしていない場合やダウンロードしたデータが見いだせない場合には、また一からやり直さなければならない」ということです。

私の場合、税務署の相談会場でおねーさんの指示に従い、「ここまでの入力内容を保存」をタップして保存できたと思っていた所、その後に「入力データをダウンロードする」というステップの際におねーさんが(お隣のサポートを行っていたので)おらず(また、そんな2段階のタッピングとなることも想定しておらず)、「保存出来た」と思って家に帰って「ファイルはどこだ?」となり、泣きました。(相談会場ではスマホを持っている人は自分のスマホを使うように指示されますが、画面はPC用なので、画面が狭いスマホでは全体像が分からず、こういうことになります。実際PCで使っていても何度もスクロールしないと必要な処理ボタンに行きつけないことがあるので画面設計にも問題があると思いますね。)

 

まぁ、最終的には(申告内容が年金生活者で単純であったこともあり)家でPCを使って再入力して申告完了しましたが、「楽になった」とはいえ、国民(納税者)ユーザーが「使いやすくなる」、「情報利便性が高まる」迄にはまだ道のりがあるように感じます。

 

ps. ところで、「国民皆背番号制」のマイナンバーは「振った(究極の普及)」ので何ら問題はないと思うのに、何故国は「マイナンバーカード」、即ち「カードを持たせること」に固執するのでしょうか?情報が化体した物(カード)を持つことは紛失、盗難、破損等のリスクが生じることは明らかであり、時代は「(物理)カードから(情報)クラウド(専用の安全サイトにあるサーバー)での保管」に移行していると感じられるのに「何故?」という違和感が感じられてしようがありません。

 

それでは前回(6,000文字を超えたので)解説できなかった所から解説を続けます。

 

    ///////////////////////////////////
    //メニュー、ツールバー関連メソッド
    ///////////////////////////////////
    //「ログファイルを開く」処理
    private void OnOpen_Click(object sender, EventArgs e)
    {
        DialogResult dr = MessageBox.Show("ログファイルを開きますか?(「はい」)\r\nそれとも総てのファイルですか?(「いいえ」)\r\n或いは中止しますか?(「キャンセル」)", "ファイル種類確認", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);
        OpenFileDialog ofDlg = new OpenFileDialog();
        ofDlg.AddExtension = true;        //拡張子自動付加
        switch(dr)
        {
        case DialogResult.Yes:
            ofDlg.FileName = "*.log";    //拡張子
            ofDlg.FilterIndex = 1;        //ファイルフィルターインデックス
            break;
        case DialogResult.No:
            ofDlg.FileName = "*.*";        //拡張子
            ofDlg.FilterIndex = 2;        //ファイルフィルターインデックス
            break;
        default:
            return;
        }
        //ファイルフィルターの指定
        ofDlg.Filter = "ログファイル(*.log)|*.log|総てのファイル(*.*)|*.*";
        ofDlg.RestoreDirectory = true;    //初期ディレクトリへ復帰
        ofDlg.CheckFileExists = true;    //ファイルの存在チェック
        ofDlg.CheckPathExists = true;    //ファイルパスの存在チェック
        ofDlg.InitialDirectory = ".";    // デフォルトのフォルダーの指定
        ofDlg.Title = "ファイルを開く";    //ダイアログのタイトルを指定する
        if(ofDlg.ShowDialog() == DialogResult.OK)    //ダイアログを表示する
        {
            SwitchManuLog(true);        //「ログ置換」設定にする
            tssl[2].ToolTipText = tssl[2].Text = ofDlg.FileName;    //ステータスバーにログファイルを表示する
        }
        else
        {
            MessageBox.Show("キャンセルされました。", "キャンセル", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
        }
        // オブジェクトを破棄する
        ofDlg.Dispose();
    }

    //「終了」処理
    private void OnExit_Click(object sender, EventArgs e)
    {
        Close();    //修了確認はOnClosingメソッドで行う
    }

    //「手動置換」処理
    private void OnRepManu_Click(object sender, EventArgs e)
    {
        if(String.IsNullOrEmpty(tbToRep.Text))
            MessageBox.Show("検索文字列が入力されていません","エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        else
            RepFilesInLV(tbToRep.Text, tbRepWith.Text);    //「検索文字列」、「置換文字列」テキストボックスで処理
    }
//解説:これが手動置換です。RepFilesInLVメソッドは「ListViewにあるファイルデータの文字列を検索文字から置換文字に変換する」処理です。


    //「ログ置換」処理(ファイルによるバッチ処理)
    private void OnRepLog_Click(object sender, EventArgs e)
    {
        //最終の実行確認を行う
        DialogResult res = MessageBox.Show("本当にログ置換を実行しますか?", "最終確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
        if(res == DialogResult.Yes)
        {
            //ログファイル(各行がカンマ区切り2つの文字列のcsvファイル)による置換処理
            Encoding enc = Encoding.GetEncoding("shift_jis");    //Sift-JISを選択しているが、変更可能
            //ログファイル(File.ReadAllText(tssl[2].Textはファイル名)を読む
            StringReader sr = new StringReader(File.ReadAllText(tssl[2].Text, enc));
            string line = string.Empty;            //一行データ読み込み用変数
            while(true)                            //無限ループ
            {
                line = sr.ReadLine();
                if(String.IsNullOrEmpty(line))    //文末チェック
                    break;
                else                            //行データがある場合
                {
                    try                            //改行コード(0X0D0A)変換余りでデータ行以外がすり抜ける場合
                    {
                        //line.Contains(',')で「値がnul」エラーが生じるリスク
                        if(!line.Contains(','))    //データ行か否かのチェック(カンマが無ければ終了)
                            break;
                        //「Indexが配列境界外」エラーリスク対処
                        string[] words = line.Split(',');
                        RepFilesInLV((tbToRep.Text = words[0]), (tbRepWith.Text = words[1]));
                    }
                    catch    //エラー発生時はループを抜け出す
                    {
                        break;
                    }
                }
            }
            sr.Close();
        }
        tssl[2].ToolTipText = tssl[2].Text = "(ログファイル名)";    //ステータスバーのファイル表示を初期化する
        SwitchManuLog(false);                //「手動置換」設定にする
        Replace_Btn.Enabled = true;            //文字列置換ボタンを処理処理後、有効にする
    }
//解説:これがログ置換処理で、読み込んだログファイルから一行切り出し、その行から検索文字列と置換文字列のデータを取り出して↑で説明した手動置換処理に行が続く限り繰り返す処理です。(前に「なんでエラーが出るんだろう?」というブログを書きましたね?最終的にtry~catchを使うことになりました。)

    //「バックアップ」処理
    private void OnSetUp_Click(object sender, EventArgs e)
    {
        if(miBackUp.Checked)
            miBackUp.Checked = false;
        else
            miBackUp.Checked = true;
    }
//解説:C#ではインスタンスのプロパティに値を持たせられるので、できる限り変数にせずにプロパティを使ってコーディングしました。miBackUpはメニュー項目のインスタンスで、これを変数代わりに使っています。

    //「使い方」処理
    private void OnHowtoUse_Click(object sender, EventArgs e)
    {
        OwnPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);    //自分自身のパスを取得
        //Process.Start("ファイル名", "引き数");も可
        ProcessStartInfo processInfo = new ProcessStartInfo
        {
            FileName = "hh",
            Arguments = OwnPath + @"\ReplacerHelp.chm"
        };
        Process.Start(processInfo);
    }
//解説:これが最後に行ったchmファイルを表示する処理です。Pathクラス、ProcessクラスのStartメソッドがあるので実に簡単です。

    //「バージョン」処理
    private void OnVersion_Click(object sender, EventArgs e)
    {
        VersionDlg verDlg = new VersionDlg(this.Icon);    //バージョンダイアログの表示
        verDlg.ShowDialog();

        verDlg.Dispose();    //2023年06月30日追記
    }

    ///////////////////////////
    //コントロール関連メソッド
    ///////////////////////////
    //「ファイルを開く」処理
    private void OnSelFile_Btn_Click(object sender, EventArgs e)
    {
        DialogResult dr = MessageBox.Show("入れ替えますか(はい)、または\r\n追加しますか(いいえ)?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
        if(dr == DialogResult.Yes)
        {
            FileList.Items.Clear();    //入替えの場合、リストビュー内の項目を全削除
        }
        OpenFileDialog ofDlg = new OpenFileDialog();
        ofDlg.FileName = "*.*";                        //初期ファイル名の指定
        ofDlg.Filter = "C#ファイル(*.cs)|*.cs|すべてのファイル(*.*)|*.*";    //ファイルフィルターの指定
        ofDlg.FilterIndex = 2;                        //ファイルフィルターインデックスの指定
        ofDlg.RestoreDirectory = true;                //ダイアログボックスを閉じる前に現在のディレクトリを復元するようにする
        ofDlg.CheckFileExists = true;                //存在しないファイルの名前が指定されたとき警告を表示する
        ofDlg.CheckPathExists = true;                //存在しないパスが指定されたとき警告を表示する
        ofDlg.Multiselect = true;                    //複数ファイルの選択を行う
        ofDlg.InitialDirectory = @".\";                // デフォルトのフォルダを指定する
        ofDlg.Title = "ファイルを開く";                //ダイアログのタイトルを指定する
        if(ofDlg.ShowDialog() == DialogResult.OK)    //ダイアログを表示する
        {
            foreach(string fn in ofDlg.FileNames)    //複数選択ファイル名コレクションの表示
            {
                ListViewItem item = FileList.Items.Add(Path.GetFileName(fn));
                item.SubItems.Add("0");
                item.SubItems.Add(fn);
            }
         }
        else
        {
            MessageBox.Show("キャンセルされました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
        // オブジェクトを破棄する
        ofDlg.Dispose();
    }

    //「フォールダーを開く」処理
    private void OnSelFolder_Btn_Click(object sender, EventArgs e)
    {
        DialogResult dr = MessageBox.Show("入れ替えますか(はい)、または\r\n追加しますか(いいえ)?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
        if(dr == DialogResult.Yes)
        {
            FileList.Items.Clear();    //入替えの場合、リストビュー内の項目を全削除
        }
        FolderBrowserDialog fbDlg = new FolderBrowserDialog();
        fbDlg.Description = "対象ファイルのあるフォルダーを選択してください。";    // ダイアログの説明文
        fbDlg.SelectedPath = ".\\";                    // デフォルトのフォルダー
        if(fbDlg.ShowDialog() == DialogResult.OK)    //フォルダを選択するダイアログを表示する
        {
            tbPath.Text = fbDlg.SelectedPath;        //「パス名」テキストボックスに表示
            //フォルダ内のファイル名だけ表示
             IEnumerable<string> files = Directory.EnumerateFiles(fbDlg.SelectedPath, "*", SearchOption.TopDirectoryOnly);
            foreach (string fn in files)            //フォールダー内ファイルの表示
            {
                ListViewItem item = FileList.Items.Add(Path.GetFileName(fn));
                item.SubItems.Add("0");
                item.SubItems.Add(fn);
            }
        }
        else
        {
            MessageBox.Show("キャンセルされました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
        // オブジェクトを破棄する
        fbDlg.Dispose();
    }

    //「行削除」処理
    private void OnListView_ItemActivate(object sender, EventArgs e)
    {
        ListView lv = (ListView)sender;
        //アイテムがアクティブになった時
        DialogResult dr = MessageBox.Show("選択項目" + lv.FocusedItem.Text + "を削除しますか?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
        if(dr == DialogResult.Yes)
        {
            //フォーカスのあるアイテムを削除する
            lv.FocusedItem.Remove();
        }
    }
//解説:ListViewのEventはいくつかありますが、ItemChange処理で一回書きましたが、矢張りItemActivateの方が感覚的に素直なのでこちらを使いました。

    //「置換ボタン」処理
    private void OnReplace_Btn_Click(object sender, EventArgs e)
    {
        if(tssl[2].Text == "(ログファイル名)")    //規定値の場合手動置換
        {
            OnRepManu_Click(sender, e);
        }
        else                                        //ログ置換
        {
            Replace_Btn.Enabled = false;            //このボタンを処理が済むまで無効にする
            OnRepLog_Click(sender, e);
        }
    }
//解説:置換処理メソッドはメニューのイベントハンドラーで書き、この置換ボタンを押された場合には「手動置換モードまたはログ置換モードのいずれかを判断してその処理を呼ぶ」ことにし、そのモード判断の条件を、特別のモード変数を設けずに、ステータスバーの3つ目の文字列プロパティで行いました。

    //「手動置換」、「ログ置換」モードの切り替え
    private void SwitchManuLog(bool flag)                //true-「ログ置換」、false-「手動置換」
    {
        if(flag)
        {
            miRepManu.Enabled = false;                    //手動置換メニューを無効にする
            miRepLog.Enabled = true;                    //ログ置換メニューを有効にする
            toolStripButton[2].Enabled = false;            //手動置換ボタンを無効にする
            toolStripButton[3].Enabled = true;            //ログ置換ボタンを有効にする
            tssl[1].Text = "ログ置換";                    //ステータスバー表示
            tssl[1].ToolTipText = "ログ置換";            //ステータスバーToolTip設定
            tbRepWith.Text = "";                        //検索文字列テキストボックスを初期化する
            tbRepWith.Enabled = false;                    //検索文字列テキストボックスを無効にする
            tbToRep.Text = "";                            //置換文字列テキストボックスを初期化する
            tbToRep.Enabled = false;                    //置換文字列テキストボックスを無効にする
            Replace_Btn.Image = (Bitmap)toolStripButton[3].Image;    //ボタンイメージの変更
        }
        else
        {
            miRepManu.Enabled = true;                    //手動置換メニューを有効にする
            miRepLog.Enabled = false;                    //ログ置換メニューを無効にする
            toolStripButton[2].Enabled = true;            //手動置換ボタンを有効にする
            toolStripButton[3].Enabled = false;            //ログ置換ボタンを無効にする
            tssl[1].Text = "手動置換";                    //ステータスバー表示
            tssl[1].ToolTipText = "手動置換";            //ステータスバーToolTip設定
            tbRepWith.Text = "";                        //検索文字列テキストボックスを初期化する
            tbRepWith.Enabled = true;                    //検索文字列テキストボックスを無効にする
            tbToRep.Text = "";                            //置換文字列テキストボックスを初期化する
            tbToRep.Enabled = true;                        //置換文字列テキストボックスを無効にする
            Replace_Btn.Image = (Bitmap)toolStripButton[2].Image;    //ボタンイメージの変更
        }
    }
//解説:これを見ていただけると手動置換、ログ置換のモードが変わると何が変わるのかが分かります。両モードがぶつからないように排他的にメニューやコントロールを有効、無効に切り替えている点に注意してください。

    /////////////////////////////////
    //ファイル文字列置換関連メソッド
    /////////////////////////////////
    //リストボックスのファイルの置換を行う

    private void RepFilesInLV(string ToRep, string RepWith)
    {
        foreach(ListViewItem item in FileList.Items)
        {
            //オリジナルファイルのバックアップ作成
            string backup = Path.ChangeExtension(item.SubItems[2].Text, ".bak");
            if(miBackUp.Checked)
                File.Copy(item.SubItems[2].Text, backup, true);    //3番目の引数をつけずに、同名ファイルがあるとIOエラーとなる
            //オリジナルファイルの変更
            int n = CountFindRepInFile(item.SubItems[2].Text, ToRep, RepWith);
            if(n > 0)
                item.SubItems[1].Text = n.ToString();    //変換回数を表示
            else    //変換されなかった場合、作成したバックアップを削除
                File.Delete(backup);
        }
    }

    //ファイルの文字列置換(または検索)回数を返す
    private int CountFindRepInFile(string filename, string ToRep, string RepWith)
    {
        //テキストファイルの中身をすべて読み込む
        string str = string.Empty;        //ファイル読み込み用変数
        using (StreamReader sr = new StreamReader(filename, Encoding.GetEncoding("shift_jis")))    // シフトJISのテキスト用
    //    using (StreamReader sr = new StreamReader(filename))    // UTF-8のテキスト用
        {
            str = sr.ReadToEnd();    // ファイルのデータを「すべて」取得する
            sr.Close();
        }
        int count = CountFindRep(ref str, ToRep, RepWith);
        using (StreamWriter sw = new StreamWriter(filename, false, Encoding.GetEncoding("shift_jis")))    // シフトJISのテキスト上書き(false)用-trueはAppend
        {
            sw.Write(str);
            sw.Close();
        }
        return count;
    }

    //文字列置換(または検索)回数を返す-最初の検索対象文字列は置換後のものとなる
    private int CountFindRep(ref string str, string ToRep, string RepWith = "")
    {
        int position, count = 0;
        position = str.IndexOf(ToRep, 0);
        while(position != -1)
        {
            count++;
            position = str.IndexOf(ToRep, position + 1);
        }
        if(!String.IsNullOrEmpty(RepWith))
            str = str.Replace(ToRep, RepWith);
        return count;
    }
//解説:この3つのメソッドがReplacerの心臓部分ですね。

//(1)最初にまずC#における文字列置換の方法を調べました。Replaceメソッドがあって簡単なのは良いのですが、Replaceメソッドでは変換回数を取得できないので、Find(C#ではIndexOfメソッドといいます)で別に調べなければなりませんでした。そしてまずこの CountFindRepメソッドを作りました。

//(2)次にこのCountFindRepメソッドを使ってファイルを読み込み、その文字列データをCountFindRepメソッドで置換し、再度にファイルを上書きする処理を行うCountFindRepInFileメソッドを作りました。

//(3)最後にListViewに表示されたファイルを一つづつ、このCountFindRepInFileメソッドで置換し、手動置換を行うRepFilesInLVメソッドを作りました。

//(4)↑で述べたように、更にログファイルに基づき「検索文字列」「置換文字列」を更新し、RepFilesInLVメソッドを行うのがログ置換になります。
}


/////////////////////
//Version ダイアログ
/////////////////////
class VersionDlg : Form
{
    public VersionDlg(Icon ico)
    {
        //ダイアログの属性設定
        this.Text = "バーション情報";
        this.ClientSize = new Size(320, 100);
        this.MaximizeBox = false;        // 最大化ボタン
        this.MinimizeBox = false;        // 最小化ボタン
        this.ShowInTaskbar = false;        //タスクバー上表示
        this.FormBorderStyle = FormBorderStyle.FixedDialog;        // 境界のスタイル
        this.StartPosition = FormStartPosition.CenterParent;    // 親フォームの中央に配置
        //コントロールの属性設定
        Button btnOK = new Button();
        btnOK.Size = new Size(40, 28);
        btnOK.Location = new Point(ClientSize.Width - btnOK.Width - 10, (ClientSize.Height - btnOK.Height) / 2);
        btnOK.Text = "OK";
        btnOK.Click += new EventHandler(OnOK_Click);
        Label imglabel = new Label();
        imglabel.Size = new Size(40, 40);
        imglabel.Location = new Point(10, (ClientSize.Height - imglabel.Height) / 2);
        imglabel.BorderStyle = BorderStyle.Fixed3D;
        imglabel.Image = ico.ToBitmap();    //親のシステムアイコン
        Label label = new Label();
        label.Size = new Size(ClientSize.Width - imglabel.Width - btnOK.Width - 40, ClientSize.Height- 20);
        label.Location = new Point(imglabel.Width + 20, (ClientSize.Height - label.Height) / 2);
        label.BorderStyle = BorderStyle.Fixed3D;
        label.Text = "Replacer Version 1.0\r\nCopyright (c) 2023 by MS C# and Ysama\r\n";
        label.TextAlign = ContentAlignment.MiddleCenter;
        label.Font = new Font("Times New Roman", 10, FontStyle.Bold);
        this.Controls.Add(btnOK);
        this.Controls.Add(imglabel);
        this.Controls.Add(label);
    }

    private void OnOK_Click(object sender, EventArgs e)
    {
        Close();
    }
}

 

ということでReplacer、さらっと解説させていただきました。

では、Replacerについて解説します。なぜこれを作るようになったかは、これをお読みください。

 

Replacerは、BCB5で書いたダイアログ版のオマージュですが、C#で書くにあたり全く同じでは面白くないので、マニュアルで文字列変換ができるだけではなく、RenamerのLogファイルを使ってバッチ処理ができるようにしてみました。

 

1.Replacer仕様

(1)C#で先般作ったSDI Skeltonをベースにする。メニューとツールバー、ステータスバーも付けてマニュアル変換(手動置換モード)に加え、バッチ処理(ログ置換モード)の処理を行えるようにする。

(2)変換対象ファイルの選択は「ファイルを開く」ダイアログで複数選択する他、フォールダーを選択して総てのファイルを選択してから不要のものをリストから削除することもできるようにする。

(3)選択した対象ファイルを表示するのはリストボックスからリストビューに格上げし、変換(文字列置換)回数も表示する。

(4)「検索文字列」と「置換文字列」はテキストボックスで手動入力して置換ボタンを押す、というオリジナルの動作に加え、「ログファイル(バッチファイル)を読み込むと、自動的にログ置換モードになり、テキストボックスも手動入力ができなくなり、ログファイル(注)による一括置換しかできなくするし、一括置換が終了すると自動的に手動置換モードに戻るようにする」

(5)またモード変更はユーザーに視覚的にわかるようにする

ことにしました。

注:ここでいう「ログファイル」とは、Renamerで排出するログファイルと同じ一行2データのCSVファイルのことを言います。

(検索文字列1),(置換文字列1)(\r\n)

(検索文字列2),(置換文字列2)(\r\n)

(検索文字列3),(置換文字列3)(\r\n)

        ・

        ・

        ・

 

2.プログラミング

まず最初に行ったのはメニューとツールバーボタンの有り様です。もともとがダイアログで必要な処理はコントロールでできるので、メニューとツールバーは基本それ以外の操作になります。ということで「ファイル」はログファイルの読み込みと終了、「置換」は手動置換とログ置換および(設定ダイアログを作るのがだるかったので)バックアップの有無、最後に「ヘルプ」で使い方とバージョン表示ですね。ツールバービットマップはBCCFormツールのTBEditorを使いこのようにしました。

次に前にC#で作ったResWriterを使い、TBEditorで作成したビットマップを

ToolBarと命名してReplacer.resourceファイルにしました。

 

後はSDI Skeltonニコーディングするだけです。ポイントとなる部分だけ解説しましょう。

 

【Replacer.cs】

//////////////////////
// Replacer.cs
//////////////////////
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Reflection;        //Assemblyを使う為
using System.Resources;            //リソース関係クラス等の使用の為
using System.IO;                //Stream関係クラス等の使用の為
using System.Diagnostics;        //Processを使用する為

///////////////////////////
//エントリーポイントクラス
///////////////////////////
class MainApp
{
    [STAThread]
    public static void Main()
    {
        Application.Run(new MainWnd());
    }
}

///////////////////////
//メインフォームクラス
///////////////////////
public partial class MainWnd : Form
{
    //クラスメンバー変数
    string OwnPath;
    MenuItem miRepManu, miRepLog;                //「手動置換」、「ログ置換」メニューアイテム
    MenuItem miBackUp;                            //「バックアップ」メニューアイテム
    ToolStrip toolStrip;                        //ツールバー
    ToolStripButton[] toolStripButton;            //ツールバーボタン
    StatusStrip statusStrip;                    //ステータスバー
    ToolStripStatusLabel[] tssl;                //ステータスバーラベル
    Button SelFile_Btn;                            //ファイル選択ボタン
    Button SelFolder_Btn;                        //フォールダー選択ボタン
    Button Replace_Btn;                            //文字列置換ボタン
    TextBox tbPath;                                //Path表示用テキストボックス
    TextBox tbToRep;                            //検索文字列用テキストボックス
    TextBox tbRepWith;                            //置換文字列用テキストボックス
    ListView FileList;                            //返還対象ファイルリスト用リストビュー
    Label lblSelFolder;                            //フォールダー選択ボタン説明ラベル
    Label lblToRep;                                //検索文字列説明ラベル
    Label lblRepWith;                            //置換文字列説明ラベル

    public MainWnd()
    {
        Assembly myOwn = Assembly.GetEntryAssembly();
        this.Icon = Icon.ExtractAssociatedIcon(myOwn.Location);    //プログラムアイコンをフォームにつける
        this.Load += new EventHandler(MainForm_Load);
        this.Text = "文字列一括返還ユーティリティ";
        this.ClientSize = new Size(480, 341);                    //Size = (496, 380);
        this.MinimumSize = new Size(496, 380);                    //最小サイズ
        this.BackColor = SystemColors.Window;
    }

    //WM_CREATE時処理
    private void MainForm_Load(object sender, EventArgs e)
    {
        //メニュー作成
        SetMenu();
        //ツールバーとステータスバー作成
        SetBars();
        //コントロール作成
        SetControls();
    }

    //WM_CLOSE時処理
    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        base.OnFormClosing(e);
        DialogResult dr = MessageBox.Show("終了しますか?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
        if(dr == DialogResult.No)
        {
            e.Cancel = true;
        }
    }

    //メニューの設定
    protected void SetMenu()
    {
        //メインメニュー作成
        MainMenu menu = new MainMenu();
        Menu = menu;
        //メニューアイテム付加
        MenuItem miFile = new MenuItem();        //「ファイル」メニュー
        miFile.Text = "ファイル(&F)";
        miFile.Index = 0;
        menu.MenuItems.Add(miFile);
        MenuItem miReplace = new MenuItem();    //「置換」メニュー
        miReplace.Text = "置換(&R)";
        miReplace.Index = 1;
        menu.MenuItems.Add(miReplace);
        MenuItem miHelp = new MenuItem();        //「ヘルプ」メニュー
        miHelp.Text = "ヘルプ(&H)";
        miHelp.Index = 2;
        menu.MenuItems.Add(miHelp);
        MenuItem miOpen = new MenuItem();        //「ログファイルを開く」メニューアイテム
        miOpen.Text = "ログファイルを開く(&O)";
        miOpen.Index = 0;
        miOpen.Click += OnOpen_Click;
        miOpen.Shortcut = Shortcut.CtrlO;
        miFile.MenuItems.Add(miOpen);
        miFile.MenuItems.Add("-");                //セパレーター
        MenuItem miExit = new MenuItem();        //「終了」メニューアイテム
        miExit.Text = "終了(&X)";
        miExit.Index = 1;
        miExit.Click += OnExit_Click;
        miExit.Shortcut = Shortcut.CtrlX;
        miFile.MenuItems.Add(miExit);
        miRepManu = new MenuItem();                //「手動置換」メニューアイテム
        miRepManu.Text = "手動置換(&M)";
        miRepManu.Index = 0;
        miRepManu.Click += OnRepManu_Click;
        miRepManu.Shortcut = Shortcut.CtrlM;
        miReplace.MenuItems.Add(miRepManu);
        miRepLog = new MenuItem();                //「ログ置換」メニューアイテム
        miRepLog.Text = "ログ置換(&C)";
        miRepLog.Index = 1;
        miRepLog.Click += OnRepLog_Click;
        miRepLog.Shortcut = Shortcut.CtrlC;
        miReplace.MenuItems.Add(miRepLog);
        miRepLog.Enabled = false;                //初期値は無効
        miReplace.MenuItems.Add("-");            //セパレーター
        miBackUp = new MenuItem();                //「設定」メニューアイテム
        miBackUp.Text = "バックアップ(&B)";
        miBackUp.Index = 2;
        miBackUp.Click += OnSetUp_Click;
        miBackUp.Checked = true;                    //メニュー項目にレ点をつける
        miBackUp.Shortcut = Shortcut.CtrlS;
        miReplace.MenuItems.Add(miBackUp);
        MenuItem miHowtoUse = new MenuItem();    //「使い方」メニューアイテム
        miHowtoUse.Text = "使い方(&U)";
        miHowtoUse.Index = 0;
        miHowtoUse.Click += OnHowtoUse_Click;
        miHowtoUse.Shortcut = Shortcut.CtrlU;
        miHelp.MenuItems.Add(miHowtoUse);
        MenuItem miVer = new MenuItem();        //「バージョン」メニューアイテム
        miVer.Text = "バージョン(&V)";
        miVer.Index = 1;
        miVer.Click += OnVersion_Click;
        miVer.Shortcut = Shortcut.CtrlV;
        miHelp.MenuItems.Add(miVer);
    }

    //ツールバーとステータスバーの設定
    protected void SetBars()
    {
        //フォームのレイアウトを一時停止
        this.SuspendLayout();
        ///////////////////////////////////
        //ツールバーとステータスバーの配置
        ///////////////////////////////////
        //ToolStripクラスインスタンスの生成
        this.toolStrip = new ToolStrip();
        //ツールバーのレイアウトを一時停止
        this.toolStrip.SuspendLayout();
        //ToolStripButton配列を作成
        this.toolStripButton = new ToolStripButton[12];
        //本プログラムの埋め込みリソースのリソースマネージャーを作成
        Assembly asm = Assembly.GetExecutingAssembly();
        ResourceManager rm = new ResourceManager("Replacer", asm);
        //ツールバービットマップの読み込み
        ImageList imgList = new ImageList();
        imgList.ImageSize = new Size(16, 15);
        imgList.Images.AddStrip((Bitmap)rm.GetObject("ToolBar"));
        imgList.TransparentColor = Color.White;
        //ToolStripButton[0]を作成
        this.toolStripButton[0] = new ToolStripButton();
        this.toolStripButton[0].Text = "開く(&O)";                                //テキスト設定
        this.toolStripButton[0].Image = (Bitmap)imgList.Images[0];                //画像設定
        this.toolStripButton[0].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
        this.toolStripButton[0].Click += OnOpen_Click;                            //Clickイベントハンドラ追加
        this.toolStrip.Items.Add(this.toolStripButton[0]);                        //ボタンを追加
        //セパレーターを挿入
        this.toolStrip.Items.Add(new ToolStripSeparator());
        //ToolStripButton[1]を作成
        this.toolStripButton[1] = new ToolStripButton();
        this.toolStripButton[1].Text = "終了(&X)";                                //テキスト設定
        this.toolStripButton[1].Image = (Bitmap)imgList.Images[1];                //画像設定
        this.toolStripButton[1].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
        this.toolStripButton[1].Click += OnExit_Click;                            //Clickイベントハンドラ追加
        this.toolStrip.Items.Add(this.toolStripButton[1]);                        //ボタンを追加
        //セパレーターを挿入
        this.toolStrip.Items.Add(new ToolStripSeparator());
        //ToolStripButton[2]を作成
        this.toolStripButton[2] = new ToolStripButton();
        this.toolStripButton[2].Text = "手動置換(&R)";                            //テキスト設定
        this.toolStripButton[2].Image = (Bitmap)imgList.Images[2];                //画像設定
        this.toolStripButton[2].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
        this.toolStripButton[2].Click += OnRepManu_Click;                        //Clickイベントハンドラ追加
        this.toolStrip.Items.Add(this.toolStripButton[2]);                        //ボタンを追加
        //ToolStripButton[3]を作成
        this.toolStripButton[3] = new ToolStripButton();
        this.toolStripButton[3].Text = "ログ置換(&R)";                            //テキスト設定
        this.toolStripButton[3].Image = (Bitmap)imgList.Images[3];                //画像設定
        this.toolStripButton[3].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
        this.toolStripButton[3].Click += OnRepLog_Click;                        //Clickイベントハンドラ追加
        this.toolStrip.Items.Add(this.toolStripButton[3]);                        //ボタンを追加
        this.toolStripButton[3].Enabled = false;                                //初期状態は無効
        //セパレーターを挿入
        this.toolStrip.Items.Add(new ToolStripSeparator());
        //ToolStripButton[4]を作成
        this.toolStripButton[4] = new ToolStripButton();
        this.toolStripButton[4].Text = "使い方(&U)";                            //テキスト設定
        this.toolStripButton[4].Image = (Bitmap)imgList.Images[4];                //画像設定
        this.toolStripButton[4].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
        this.toolStripButton[4].Click += OnHowtoUse_Click;                        //Clickイベントハンドラ追加
        this.toolStrip.Items.Add(this.toolStripButton[4]);                        //ボタンを追加
        //ToolStripButton[5]を作成
        this.toolStripButton[5] = new ToolStripButton();
        this.toolStripButton[5].Text = "バージョン情報(&V)";                    //テキスト設定
        this.toolStripButton[5].Image = (Bitmap)imgList.Images[5];                //画像設定
        this.toolStripButton[5].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
        this.toolStripButton[5].Click += OnVersion_Click;                        //Clickイベントハンドラ追加
        this.toolStrip.Items.Add(this.toolStripButton[5]);                        //ボタンを追加
        //ツールバーの設定
        this.Controls.Add(this.toolStrip);
        //ツールバーのレイアウトを再開
        this.toolStrip.ResumeLayout(false);
        this.toolStrip.PerformLayout();

        //StatusStripクラスインスタンスの生成
        this.statusStrip = new StatusStrip();
        //ステータスバーのレイアウトを一時停止
        this.statusStrip.SuspendLayout();
        //ステータスバーにパネルとテキストを追加
        tssl = new ToolStripStatusLabel[3];
        tssl[0] = new ToolStripStatusLabel();
        tssl[0].BorderSides = ToolStripStatusLabelBorderSides.All;
        tssl[0].BorderStyle = Border3DStyle.SunkenInner;
        tssl[0].BackColor = SystemColors.Control;
        tssl[0].Text = "Replacer Ver. 1.0";
        tssl[0].AutoSize = true;
        tssl[0].TextAlign = ContentAlignment.MiddleLeft;
        tssl[1] = new ToolStripStatusLabel();
        tssl[1].BorderSides = ToolStripStatusLabelBorderSides.All;
        tssl[1].BorderStyle = Border3DStyle.SunkenInner;
        tssl[1].BackColor = SystemColors.Control;
        tssl[1].Text = "手動置換";
        tssl[1].ToolTipText = "手動置換";                //ToolTip設定
        tssl[1].AutoSize = true;
        tssl[1].TextAlign = ContentAlignment.MiddleLeft;
        tssl[2] = new ToolStripStatusLabel();
        tssl[2].BorderSides = ToolStripStatusLabelBorderSides.All;
        tssl[2].BorderStyle = Border3DStyle.SunkenInner;
        tssl[2].BackColor = SystemColors.Control;
        tssl[2].Text = "(ログファイル名)";
        tssl[2].ToolTipText = "(ログファイル名)";        //ToolTip設定
        tssl[2].Spring = true;
        tssl[2].TextAlign = ContentAlignment.MiddleLeft;
        statusStrip.Items.AddRange(tssl);
        statusStrip.ShowItemToolTips = true;            //ToolTip表示
        this.Controls.Add(this.statusStrip);            //StatusStrip(ステータスバー)を追加
        //ステータスバーのレイアウトを再開
        this.statusStrip.ResumeLayout(false);
        this.statusStrip.PerformLayout();
        //メインフォームのレイアウトを再開
        this.ResumeLayout(false);
        this.PerformLayout();
    }

    //コントロールの設定
    protected void SetControls()
    {
        /////////////////////
        //コントロールの配置
        /////////////////////
        //ファイル選択ボタン
        SelFile_Btn = new Button();
        SelFile_Btn.Size = new Size(140, 30);
        SelFile_Btn.Location = new Point(10, toolStrip.Height + 10);
        SelFile_Btn.Anchor = (AnchorStyles.Top | AnchorStyles.Left);
        SelFile_Btn.Text = "対象ファイルを選択";
        SelFile_Btn.Image = toolStripButton[0].Image;
        SelFile_Btn.TextImageRelation = TextImageRelation.ImageBeforeText;
        SelFile_Btn.TabStop = true;
        SelFile_Btn.TabIndex = 0;
        SelFile_Btn.Click += OnSelFile_Btn_Click;
        this.Controls.Add(SelFile_Btn);
        //フォールダー選択ボタン説明ラベル
        lblSelFolder = new Label();
        lblSelFolder.Size = new Size(200, 30);
        lblSelFolder.Location = new Point(this.ClientSize.Width - lblSelFolder.Width, toolStrip.Height + 5);
        lblSelFolder.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
        lblSelFolder.Text = "フォールダー内全ファイルを選択\r\n(削除するにはクリックで行を選択)";
        lblSelFolder.TextAlign = ContentAlignment.MiddleCenter;
        lblSelFolder.TabStop = false;
        this.Controls.Add(lblSelFolder);
        //フォールダー選択ボタン
        SelFolder_Btn = new Button();
        SelFolder_Btn.Size = new Size(30, 30);
        SelFolder_Btn.Location = new Point(this.ClientSize.Width - SelFolder_Btn.Width - 10, toolStrip.Height + SelFile_Btn.Height + 5);
        SelFolder_Btn.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
        SelFolder_Btn.Image = toolStripButton[0].Image;
        SelFolder_Btn.TabStop = true;
        SelFolder_Btn.TabIndex = 1;
        SelFolder_Btn.Click += OnSelFolder_Btn_Click;
        this.Controls.Add(SelFolder_Btn);
        //Path表示用テキストボックス
        tbPath = new TextBox();
        tbPath.Size = new Size(ClientSize.Width - SelFolder_Btn.Width - 25, 20);
        tbPath.Location = new Point(10, toolStrip.Height + SelFile_Btn.Height + 15);
        tbPath.Anchor = (AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right);
        tbPath.Text = "";
        tbPath.ReadOnly = true;
        tbPath.TabStop = false;
        this.Controls.Add(tbPath);
        //リストビューの生成とプロパティの設定
        //https://dot-sharp.com/csharp-listview-event-order/
        FileList = new ListView();
        FileList.Size = new Size(ClientSize.Width - 20, ClientSize.Height - 170);    //サイズ
        FileList.Location = new Point(10,  toolStrip.Height + SelFile_Btn.Height + tbPath.Height + 20);    //位置
        FileList.Anchor = (AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Bottom | AnchorStyles.Right);
        FileList.FullRowSelect = true;                    //一行選択
        FileList.GridLines = true;                        //グリッドラインの表示
        FileList.Sorting = SortOrder.Ascending;            //並び替え表示
        FileList.HoverSelection = false;                //ポイントで選択できるようにする
        FileList.Activation = ItemActivation.OneClick;    //シングルクリック選択
        FileList.CheckBoxes = false;                    //チェックボックス有効化
        FileList.View = View.Details;                    //リストビューの表示方法
        FileList.TabStop = false;
        FileList.ItemActivate += new EventHandler(OnListView_ItemActivate);
        //ヘッダー定義
        FileList.Columns.Add("ファイル名", 380, HorizontalAlignment.Left);
        FileList.Columns.Add("変換回数", 76, HorizontalAlignment.Left);
        FileList.Columns.Add("詳細", 540, HorizontalAlignment.Left);
        //FormにListViewを追加
        this.Controls.Add(FileList);
        //置換文字列説明ラベル
        lblRepWith = new Label();
        lblRepWith.Location = new Point(10, ClientSize.Height - statusStrip.Height - lblRepWith.Height);
        lblRepWith.Size = new Size(75, 20);
        lblRepWith.Anchor = (AnchorStyles.Bottom | AnchorStyles.Left);
        lblRepWith.Text = "置換文字列";
        lblRepWith.TextAlign = ContentAlignment.MiddleLeft;
        lblRepWith.TabStop = false;
        this.Controls.Add(lblRepWith);
        //検索文字列説明ラベル
        lblToRep = new Label();
        lblToRep.Location = new Point(10, ClientSize.Height - statusStrip.Height - lblToRep.Height - lblRepWith.Height);
        lblToRep.Size = new Size(75, 20);
        lblToRep.Anchor = (AnchorStyles.Bottom | AnchorStyles.Left);
        lblToRep.Text = "検索文字列";
        lblToRep.TextAlign = ContentAlignment.MiddleLeft;
        lblToRep.TabStop = false;
        this.Controls.Add(lblToRep);
        //文字列置換ボタン
        Replace_Btn = new Button();
        Replace_Btn.Size = new Size(70, 35);
        Replace_Btn.Location = new Point(this.ClientSize.Width - Replace_Btn.Width - 10, ClientSize.Height - statusStrip.Height - Replace_Btn.Height - 7);
        Replace_Btn.Anchor = (AnchorStyles.Bottom | AnchorStyles.Right);
        Replace_Btn.Image = toolStripButton[2].Image;
        Replace_Btn.TabStop = true;
        Replace_Btn.TabIndex = 4;
        Replace_Btn.Click += OnReplace_Btn_Click;
        this.Controls.Add(Replace_Btn);
        //置換文字列用テキストボックス
        tbRepWith = new TextBox();
        tbRepWith.Location = new Point(85, ClientSize.Height - statusStrip.Height - tbRepWith.Height - 5);
        tbRepWith.Size = new Size(ClientSize.Width - Replace_Btn.Width - lblRepWith.Width - 25, 20);
        tbRepWith.Anchor = (AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right);
        tbRepWith.Text = "";
        tbRepWith.TabStop = true;
        tbRepWith.TabIndex = 3;
        this.Controls.Add(tbRepWith);
        //検索文字列用テキストボックス
        tbToRep = new TextBox();
        tbToRep.Location = new Point(85, ClientSize.Height - statusStrip.Height - tbToRep.Height - tbRepWith.Height - 5);
        tbToRep.Size = new Size(ClientSize.Width - Replace_Btn.Width - lblToRep.Width - 25, 20);
        tbToRep.Anchor = (AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right);
        tbToRep.Text = "";
        tbToRep.TabStop = true;
        tbToRep.TabIndex = 2;
        this.Controls.Add(tbToRep);
    }

ここまでがウィンドウのどんがら処理です。(文字数がリミットを超えたののでここまでにさせていただきます。)

 

ECCSkeltonでRenamerを作り、ファイル名変換ログを残すようにしました。

 

そして、ログをもとに関連ファイルの文字列置換を行おうと、C#でReplacerを開発する、と書きましたが、今般何とかReplacerが組みあがったので後は簡単でもヘルプファイルを作っておこうと考えました。

(手動置換画面)

(ログ置換画面)

 

ここまでは良かったのですが、ヘルプファイルを作って無事コンパイルし、ヘルプファイルを開いたら、

あらら???

なんで?(エラーもないのに)どうして?

 

まるっきり訳が分からないので、webをググります。曰く、

(1)再起動しなさい→何の役にも立たず。

(2)新しいHTML Help Workshopをダウンロードしなさい→私の、最新なんですが?

(3)レジストリーをいじれ、DLLをDLしろ→???やばいことはやらない方がよい。

ということで6-7つ記事を読んでも何の役にも立たず。最後にこの記事を発見。(どうも言葉がおかしいので英文記事の自動翻訳か?)HTMLエディター、HTML Help Workshop、chmファイルは正常に動くので「これ(注)はあるかも?」と感じ、実験してみました。

注:私のフォールダー構成は「Programing」フォールダーの中に「Active Basic」、「Borland C++」、「C Programing」、「C++ Programing」、「C# Programing」、「Visual Basic」、「Windows Programing」というフォールダーで言語別に分けられ、当該ファイルは「C# Programing」のサブフォールダーに入っています。

 

ReplacerとReplacerHelp.chmだけを「C# Programing」フォールダーの外に出して動作させると、

あ~ら、不思議???

ちゃんと表示されました!

 

ということで、HTMLファイル関係をC#プログラムで使うなら、フォールダーは"CS"とか"CSharp"にしておくことが無難です。

 

Renamerプロジェクトの最後にRenamerProc.hを解説します。(関数名が紫色なのは、ユーザー定義関数で後で出てきますよ、という意味です。)

 

【RenamerProc.h】

//////////////////////////////////////////
// RenamerProc.h
// Copyright (c) 05/06/2020 by ECCSkelton
//////////////////////////////////////////

/////////////////////////////////
//主ウィンドウCMyWndの関数の定義
//ウィンドウメッセージ関数
/////////////////////////////////

bool CMyWnd::OnInit(WPARAM wParam, LPARAM lParam) {

    //IDC_PATHにツールチップをつける
    g_Tip.SetToolTip(m_hWnd, IDC_PATH, L"フォールダーを選択してください", TRUE);
//解説:ファイルパスを表示するエディットコントロールに外部変数のツールチップクラスインスタンスを設定します。

    //ファイル名変更ダイアログボタンを無効化する
    CheckList();
//解説:ファイル名変更ダイアログボタンの無効化、有効化切り替えはCheckListという関数にまとめました。後で説明します。

    //ヘルプファイルの設定
    CARG arg;
    g_HelpFile = arg.Path();
    g_HelpFile = g_HelpFile + L"\\RenamerHelp.chm";

//解説:CARGクラスを使って自らのパスにヘルプファイル名を付加しています。
    //ログファイルフォールダーの存在確認(ない場合は作成する)
    g_LogPath = arg.Path();
    g_LogPath = g_LogPath + L"\\RenameLog\\";
    if(!PathFileExistsW(g_LogPath.ToChar()))
        CreateDirectoryW(g_LogPath.ToChar(), NULL);

//解説:結構定番処理です。覚えておいて損はないです。
    //ドラッグアンドドロップを許可する
    DragAcceptFiles(m_hWnd, TRUE);
    return TRUE;
}

bool CMyWnd::OnDropFiles(WPARAM wParam, LPARAM lParam) {

    //入替か追加の確認
    int res = MessageBoxW(m_hWnd, L"ファイルを入れ替えますか(はい)、または追加しますか(いいえ)",
                        L"入替か追加の確認", MB_YESNOCANCEL | MB_ICONQUESTION);
    if(res == IDYES) {
        //g_Pathを初期化
        g_Path = L"";
        //リストボックス内掃除
        int i = SendItemMsg(IDC_LISTBOX, LB_GETCOUNT, 0, 0);
        if(i > 0)
            for(--i; i >= 0; i--)
                SendItemMsg(IDC_LISTBOX, LB_DELETESTRING, (WPARAM)i, 0);
    }

    else if(res == IDCANCEL)
        return FALSE;

//解説:リストボックスにデータを追加する場合、前のデータがあることが考えられ、消去するか否かの確認です。

    //ドラッグアンドドロップされたファイル名取得と処理
    WCHAR fn[MAX_PATH];
    //ドラッグアンドドロップされたファイルの数取得
    int num = DragQueryFileW((HDROP)wParam, 0xFFFFFFFF, NULL, 0);
    //ドラッグアンドドロップされたファイル名取得と処理
    for(int i = 0; i < num; i++) {
        DragQueryFileW((HDROP)wParam, i, fn, sizeof(fn));
        //リストボックスにファイルを追加する
        WCHAR** pathname = GetPathName(fn);

//解説:GetPathNameは、パス名を分解し、パスと名を二つの文字列で返すユーザー定義関数です。後で解説します。

       if(*g_Path.ToChar() && g_Path != pathname[0])
            MessageBoxW(m_hWnd, L"異なるフォールダーのファイルを追加できません。", pathname[1], MB_OK | MB_ICONERROR);
//解説:Renamerは基本同一フォールダー内のファイルを対象にしているので、このようなエラーチェックを入れています。

        else {
            if(!*g_Path.ToChar()) {
                g_Path = pathname[0];
                SendItemMsg(IDC_PATH, WM_SETTEXT, 0, (LPARAM)pathname[0]);
                g_Tip.UpdateText(pathname[0]);

//解説:データパス(対象ファイルのフォールダー)が未だ設定されていなければ最初のファイルのパスを設定します。

            }
            SendItemMsg(IDC_LISTBOX, LB_ADDSTRING, 0, (LPARAM)pathname[1]);

//解説:リストボックスに設定する設定します。

        }
    }
    DragFinish((HDROP)wParam);
    //ファイル名変更ボタンを有効化する
    CheckList();
//解説:ユーザー定義関数は後で解説します。

    return TRUE;
}

bool CMyWnd::OnClose(WPARAM wPram, LPARAM lParam) {

    //終了確認
    if(MessageBoxW(m_hWnd, L"終了しますか", L"終了確認",
                MB_YESNO | MB_ICONINFORMATION) == IDYES) {
    //処理をするとDestroyWindow、PostQuitMessageが呼ばれる
        Destroy();

//解説:Destroy関数を呼ぶとウィンドウを破棄するのでWM_DESTROYメッセージを出し、OnDestroy関数が呼ばれます。

        return TRUE;
    }
    else
        return FALSE;
}

bool CMyWnd::OnDestroy(WPARAM wPram, LPARAM lParam) {

    //ダイアログベースの場合はこれが必要
    PostQuitMessage(0);

//解説:プログラムインスタンスを終了させるメッセージを置きます。ウィンドプではDefWindowProc関数が行いますが、モードレスダイアログはユーザーがマニュアルで行います。

    return TRUE;
}

/////////////////////////////////
//主ウィンドウCMyWndの関数の定義
//メニュー項目、コントロール関数
/////////////////////////////////

bool CMyWnd::OnSelect() {

    //フォールダー選択とフォールダー内ファイルのリストボックス登録
    WCHAR *fp = g_CmnDlg.GetPath(m_hWnd, L"ファイルパスの選択");
    if(fp)
        g_Path = fp;

//解説:g_Pathに対象ファイルのあるフォールダーへのパスが記録されます。

    else {
        MessageBoxW(m_hWnd, L"パスが選択されていません", L"エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
    //リストボックス表示
    RenewListBox();

//解説:RenewListBoxユーザー定義関数です。後で解説します。

    //g_OrgFilesを更新し、ファイル名変更ダイアログボタンを有効化する
    CheckList();
    return TRUE;
}

bool CMyWnd::OnListBox(WPARAM wParam) {

    if(HIWORD(wParam) == LBN_SELCHANGE) {
        //ポップアップメニューを読みこむ
        HMENU hMenu = LoadMenuW(m_hInstance, L"IDM_POPUP");
        HMENU hPopupMenu = GetSubMenu(hMenu, 0);
        //ポップアップメニューの表示
        POINT pt;
        GetCursorPos(&pt);
        TrackPopupMenu(hPopupMenu, TPM_LEFTALIGN | TPM_LEFTBUTTON,
                      pt.x, pt.y, 0, m_hWnd, NULL);
        //メニューリソースの開放
        DestroyMenu(hMenu);
        return TRUE;

//解説:リストボックスからのメッセージはWM_COMMANDで出されるので、この中から選択行が変更された場合のもの(LBN_SELCHANGE)だけ取り出して、ポップアップメニューを表示し、ファイルの削除または追加を可能にします。

    }
    return FALSE;
}

bool CMyWnd::OnReplace() {

    subdlg.DoModal(m_hWnd, L"IDD_SUB", GetInstance());
    //結果確認のためにカレントフォールダーを表示
    RenewListBox();

//解説:「文字列の置換」ボタンが押されると旧ファイル名の入ったg_OrgFilesと、新ファイル名の入ったg_NewFilesを処理するIDD_SUBダイアログを表示します。ダイアログ処理が終わるとファイル置換処理後のフォールダー内のファイルを表示します。

    return TRUE;
}

bool CMyWnd::OnSerial() {

    sub2dlg.DoModal(m_hWnd, L"IDD_SUB", GetInstance());
    //結果確認のためにカレントフォールダーを表示
    RenewListBox();
//解説:同じような処理ですが、「同名連番付け」では同じダイアログを異なるダイアログクラスでラップしていることにご注意ください。

    return TRUE;
}

bool CMyWnd::OnHelp() {

    CSTR cmd = L"hh";
    if(ShellExecuteW(m_hWnd, L"open", cmd.ToChar(), g_HelpFile.ToChar(), NULL, SW_SHOWNORMAL) <= (HINSTANCE)32) {    //L"open" or L"runas"
        MessageBoxW(m_hWnd, L"ヘルプファイルが開けませんでした", L"エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
    else
        return TRUE;
//解説:ヘルプファイルの表示処理です。

}

bool CMyWnd::OnExit() {

    SendMsg(WM_CLOSE, 0, 0);
    return TRUE;
//解説:終了処理です。

}

bool CMyWnd::OnDel() {

    int sel = SendItemMsg(IDC_LISTBOX, LB_GETCURSEL, 0, 0);
    SendItemMsg(IDC_LISTBOX, LB_DELETESTRING, (WPARAM)sel, 0);
    CheckList();
    return TRUE;
//解説:選択行をselにとり、それを削除して後にリストボックスに基づき、ファイルを更新しています。

}

bool CMyWnd::OnAdd() {

    WCHAR *fn = g_CmnDlg.GetFileName(m_hWnd, L"すべてのファイル\0*.*\0\0", TRUE);
    if(!fn) {
        MessageBoxW(m_hWnd, L"キャンセルされました。", L"エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
    WCHAR** pathname = GetPathName(fn);
    if(*g_Path.ToChar() && g_Path != pathname[0]) {
        MessageBoxW(m_hWnd, L"異なるフォールダーのファイルを追加できません。", pathname[1], MB_OK | MB_ICONERROR);
        return FALSE;
    }
    else {
        SendItemMsg(IDC_LISTBOX, LB_ADDSTRING, 0, (LPARAM)pathname[1]);
        CheckList();
        return TRUE;
    }

//解説:ファイルの追加はコモンダイアログを利用し、追加後のリストボックスに基づき、ファイルを更新しています。

}

////////////////////
//ユーザー関数定義
/////////////////////////////////
//pathnameをpathとnameに分解して
//ポインター配列を返す
/////////////////////////////////

WCHAR** CMyWnd::GetPathName(WCHAR* pathname) {

    static WCHAR buffer[MAX_PATH], *ret[2];
    lstrcpyW(buffer, pathname);
    WCHAR* lastBackSlash;
    for(WCHAR* cp = buffer; *cp; cp++)
        if(*cp == '\\')
            lastBackSlash = cp;
    *lastBackSlash = L'\0';
    ret[0] = buffer;
    ret[1] = ++lastBackSlash;
    return ret;
//解説:先ずstatic変数(スタックではなく、個別の記憶領域を確保される)を用意し、ファイルパス名文字列と二つの文字列ポインターを確保し、引数のファイルパス名を文字列変数にコピーし、それを使って最後のバックスラッシュ(\\)アドレスを特定して文字列区切のnullを置き、文字列アドレス配列の第1アドレスにパスを。第二に名を入れて文字列配列アドレスを返します。

}


/////////////////////////////////////////
//path内のファイルをリストボックスに表示
//同時にg_OrgFilesにファイル名をコンマ区
//切り文字列で記録
/////////////////////////////////////////

void CMyWnd::RenewListBox() {

    //リストボックス内掃除
    int i = SendItemMsg(IDC_LISTBOX, LB_GETCOUNT, 0, 0);
    if(i > 0) {
        for(--i; i >= 0; i--)
            SendItemMsg(IDC_LISTBOX, LB_DELETESTRING, (WPARAM)i, 0);
    }
    //リストボックスにカレントフォールダーのファイル名を表示
    CSTR path = g_Path + L"\\*.*";        //ファイル名ベースにしないと機能しない
    MakeOrgFileList(path.ToChar());
    //変更予定ファイル名の登録
    ListFiles(m_hWnd, IDC_LISTBOX, &g_OrgFiles);
//解説:この関数はまずリストボックスを総て消去し、ファイル検索関数MakeOrgFileList関数で指定フォールダー内のファイルを検索してg_OrgFIlesを更新し、最後にUser.hの共通関数ListFilesでリストボックスに表示しています。

}

/////////////////////////////////////////
//カレントフォールダーのファイルをリスト
//ボックスに表示
/////////////////////////////////////////

bool CMyWnd::MakeOrgFileList(WCHAR* path) {

    WIN32_FIND_DATAW fd;
    HANDLE hFind = FindFirstFileW(path, &fd);
    int i = 0;                //ファイル数カウンター
    if (hFind == INVALID_HANDLE_VALUE) {
        MessageBoxW(m_hWnd, L"ファイル情報取得失敗", L"エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
    else {
        g_OrgFiles = L"";
        do {
            if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY);    // Directory
            else {                                                    // Files
                g_OrgFiles = g_OrgFiles + L"\"" + fd.cFileName + L"\",";    //引用符で括る
            }
        } while (FindNextFileW(hFind, &fd));
    }
    FindClose(hFind);
    return (bool)i;
//解説:これは内部処理用の関数で、FindFirstFileWとFIndNextFileW関数を使って指定フォールダー内のファイルを調べ(フォールダーは除外しています→"Directory"の部分参照)、ファイル名を引用符("")をつけてコンマ区切りで並べてゆきます。(CSVファイルになっています。)

}

///////////////////////////////////////////////
//リストボックスのファイルを数え、g_OrgFilesを
//更新し、ファイル名変更ボタンを有無効化する
///////////////////////////////////////////////

bool CMyWnd::CheckList() {

    //リストボックス内のファイル数を確認
    int n = SendItemMsg(IDC_LISTBOX, LB_GETCOUNT, 0, 0);
    if(n) {
        //IDC_PATHにファイル名を表示し、ツールチップをつける
        SendItemMsg(IDC_PATH, WM_SETTEXT, 0, (LPARAM)g_Path.ToChar());
        g_Tip.UpdateText(g_Path.ToChar());
        //g_OrgFilesを更新する
        g_OrgFiles = L"";
        WCHAR str[MAX_PATH];
        for(int i = 0; i < n; i++) {
            g_OrgFiles = g_OrgFiles + L"\"";
            SendItemMsg(IDC_LISTBOX, LB_GETTEXT, i, (LPARAM)str);
            g_OrgFiles = g_OrgFiles + str + L"\",";
        }
        //ファイル名変更ダイアログボタンを有効化する
        EnableWindow(GetDlgItem(m_hWnd, IDC_REPLACE), TRUE);
        EnableWindow(GetDlgItem(m_hWnd, IDC_SERIAL), TRUE);
    }
    else {
        //IDC_PATHとツールチップを初期化する
        g_Path = L"";
        SendItemMsg(IDC_PATH, WM_SETTEXT, 0, (LPARAM)g_Path.ToChar());
        g_Tip.UpdateText(L"フォールダーを選択してください");
        //ファイル名変更ダイアログボタンを無効化する
        EnableWindow(GetDlgItem(m_hWnd, IDC_REPLACE), FALSE);
        EnableWindow(GetDlgItem(m_hWnd, IDC_SERIAL), FALSE);
    }
    return (bool)n;
//解説:この関数はリストボックスをチェックし、ファイル名がリストされておれば、g_Pathに基づいて津^-ルチップとIDC?PAHを設定し、g_OrgFilesをリストボックスにより更新し(削除や追加などが行われると変更が生じるため)、最後にhな間対象ファイルがあるのでボタンを有効化します。逆に、リストボックスにファイルが無ければg_Path、ツールチップとIDC_PATHを初期化し、ファイル名変更ボタンを無効化します。

}

//////////////////////////////
//SUBダイアログの関数の定義
//コントロール関数
//////////////////////////////

bool SUBDLG::OnInit(WPARAM wParam, LPARAM lParam) {

    //「大文字小文字を区別」にチェックを入れる
    SendItemMsg(IDC_CHECKUORL, BM_SETCHECK, BST_CHECKED, (LPARAM)0);
    //「変換実行」ボタンを無効にする
    EnableWindow(GetDlgItem(m_hWnd, IDOK), FALSE);
    //変更予定ファイル名の登録
    ListFiles(m_hWnd, IDC_LISTBOX, &g_OrgFiles);
    return TRUE;
//解説:これらはダイアログの初期設定です。

}

bool SUBDLG::OnPreview() {

    //「大文字小文字を区別」の状態を確認し、記録UorLに記録
    LRESULT UorL = SendItemMsg(IDC_CHECKUORL, BM_GETCHECK, 0, (LPARAM)0);
    //リストボックスから元のファイル名を取得
    int n = SendItemMsg(IDC_LISTBOX, LB_GETCOUNT, 0, 0);
    if(!n) {
        MessageBoxW(m_hWnd, L"ファイルがありません", L"エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
    else {
        //検索文字、置換文字と作業バッファ
        WCHAR ToRep[MAX_PATH], RepWith[MAX_PATH], buff[MAX_PATH];
        //検索文字をエディットボックスから取得
        GetWindowTextW(GetDlgItem(m_hWnd, IDC_TOREPLACE), ToRep, (LPARAM)MAX_PATH);
        if(!*ToRep) {
            MessageBoxW(m_hWnd, L"置換対象文字が未入力です。", L"エラー", MB_OK | MB_ICONERROR);
            return FALSE;
        }
        else
            if(UorL == BST_UNCHECKED)    //大文字小文字を区別しない場合、ToRepを小文字化する
                CharLowerW(ToRep);
        //置換文字をエディットボックスから取得
        GetWindowTextW(GetDlgItem(m_hWnd, IDC_REPLACEWITH), RepWith, (LPARAM)MAX_PATH);
        if(!*RepWith) {
            MessageBoxW(m_hWnd, L"置換文字が未入力です。", L"エラー", MB_OK | MB_ICONERROR);
            return FALSE;
        }
        //元のコンマ(',')区切りファイル名リスト(g_OrgFiles)でg_OldFilesを初期化
        g_OldFiles = g_OrgFiles;    //初期化
        g_OrgFiles = L"";            //初期化
        g_NewFiles = L"";            //初期化
        CSTR Name, work;            //コンマ区切りファイル名切り出し変数と小文字変換用変数
        for(int i = 0; g_OldFiles.Next(Name); i++) {
            work = Name;                            //Nameを保持し、workを作業用に使う
            if(UorL == BST_UNCHECKED)                //大文字小文字を区別しないならば、
                CharLowerW(work.ToChar());            //workの文字列を小文字化する
            //置換して、新しいファイル名リスト(g_NewFiles)をコンマ(',')区切りで作成
            if(InstrRep(work.ToChar(), ToRep, RepWith, buff)) {
                g_OrgFiles = g_OrgFiles + L"\"";            //引用符括り
                g_OrgFiles = g_OrgFiles + Name.ToChar();    //置換対象後ファイル名
                g_OrgFiles = g_OrgFiles + L"\",";            //引用符括りとコンマ区切り
                g_NewFiles = g_NewFiles + L"\"";            //引用符括り
                g_NewFiles = g_NewFiles + buff;                //変更後ファイル名
                g_NewFiles = g_NewFiles + L"\",";            //引用符括りとコンマ区切り
            }
        }
        ListFiles(m_hWnd, IDC_LISTBOX, &g_NewFiles);    //新しいファイル名をリストする
        //「変換実行」ボタンを有効にする
        EnableWindow(GetDlgItem(m_hWnd, IDOK), TRUE);
        return TRUE;
//解説:コメントがほぼすべて語っているので追加することはないと思いますが、g_OrgFilesをg_OldFilesへ作業用ファイルとしてコピーし、検索文字列が見つかったファイルをg_OrgFilesに、置換文字列に置き換えたファイルをg_OldFilesにCSV形式で入れてゆき、完了したらリストボックスに表示する処理です。

    }
}

bool SUBDLG::OnOk() {

    if(RenameFiles())
        MessageBoxW(m_hWnd, L"RenameLogフォールダーにログファイルを記録しました。", L"ログ保存確認", MB_OK | MB_ICONINFORMATION);
    else
        MessageBoxW(m_hWnd, L"ファイル名変更に失敗しました。", L"エラー", MB_OK | MB_ICONERROR);
    EndModal(TRUE);
    return TRUE;
}

bool SUBDLG::OnCancel() {

    EndModal(FALSE);
    return TRUE;
}
//解説:OnOKとOnCancelはSUBDLG、SUB2DLGに共通の処理です。OnOKはRenameFilesというUser.hにある共通のファイル名変更処理を行い、OnCancelは何もしないで帰ります。なお、RenameFiles()仕様変更を行いました。末尾参照ください。
 

///////////////////
//ユーザー定義関数
///////////////////////////////////////////////////////////////////
//文字列(Old)内の検索文字列(ToFind)を置換文字列(ToReplace)で
//1回置き換え、その結果を新文字列(New)に書き込む
//【戻り値】
//置換に成功した場合:検索文字列が見つかったアドレス
//置換に失敗した場合:0
///////////////////////////////////////////////////////////////////

WCHAR* SUBDLG::InstrRep(WCHAR* Old, WCHAR* ToFind, WCHAR* ToReplace, WCHAR* New) {

    if(!(*Old & *ToFind & *ToReplace))            // Error check
        return 0;
    WCHAR *p1 = Old, *p2 = ToFind, *findp = 0;    // Setting work pointers
    do {
        if(findp) {                                // In the course of matching
            if(!*p2) {                            // If Matching completes,
                int i;
                for(i = 0; i < (findp - Old); i++)
                    New[i] = Old[i];            // Copy Old to New till findp
                New[i] = 0;                        // Add null termination
                wcscat(New, ToReplace);            // Copying ToReplace in place of ToFind
                wcscat(New, p1);                // Copying the rest of Old
                return findp;
            }
            else                                 // In case that Matching fails
                if(*p1 != *p2) {
                    p2 = ToFind;                // Reset pointers and repeat
                    findp = 0;
                }
        }
        if(*p1 == *p2) {                        // If matching begins,
            if(!findp)                            // For the 1st time,
                findp = p1;                        // Record p1 where ToFind is found
            ++p2;                                // p2 proceeds, as p1 does
        }
    } while(*p1++);
    return 0;
}
//解説:何をしているのかは表題コメントの通りです。いかにもC++らしい処理で、これは確かCSTRのコードかなんかを流用していると思います。途中のコメントは英語で書いていますが参考にしてください。

//////////////////////////////
//SUB2ダイアログの関数の定義
//コントロール関数
//////////////////////////////

bool SUB2DLG::OnInit(WPARAM wParam, LPARAM lParam) {

    //ダイアログタイトルの変更
    SendMsg(WM_SETTEXT, 0, (LPARAM)L"同一ファイル名で連番付け");
    //使わないコントロールを隠す
    ShowWindow(GetDlgItem(m_hWnd, IDC_CHECKUORL), SW_HIDE);
    ShowWindow(GetDlgItem(m_hWnd, IDC_REPLACEWITH), SW_HIDE);
    SendItemMsg(IDC_LABEL1, WM_SETTEXT, 0, (LPARAM)L"統一ファイル名");
    SendItemMsg(IDC_LABEL2, WM_SETTEXT, 0, (LPARAM)L"注:拡張子(.*)は付けない");
    //「変換実行」ボタンを無効にする
    EnableWindow(GetDlgItem(m_hWnd, IDOK), FALSE);
    //変更予定ファイル名の登録
    ListFiles(m_hWnd, IDC_LISTBOX, &g_OrgFiles);
    return TRUE;
//解説:SUBDLGを使って別のダイアログに見せかける初期設定です。いらないコントロールは隠し、ラベルの文字列を変えています。

}

bool SUB2DLG::OnPreview() {

    //リストボックスから元のファイル名を取得
    int n = SendItemMsg(IDC_LISTBOX, LB_GETCOUNT, 0, 0);
    if(!n) {
        MessageBoxW(m_hWnd, L"ファイルがありません", L"エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
    else {
        //統一ファイル名と連番文字列(L"(???)\0"-6桁)
        WCHAR ToRep[MAX_PATH], num[6];
        //検索文字をエディットボックスから取得
        GetWindowTextW(GetDlgItem(m_hWnd, IDC_TOREPLACE), ToRep, (LPARAM)MAX_PATH);
        if(!*ToRep) {
            MessageBoxW(m_hWnd, L"統一ファイル名が未入力です。", L"エラー", MB_OK | MB_ICONERROR);
            return FALSE;
        }
        //元のコンマ(',')区切りファイル名リスト(g_OrgFiles)でg_OldFilesを初期化
        g_OldFiles = g_OrgFiles;    //初期化
        g_NewFiles = L"";            //初期化
        CSTR Name;                    //コンマ区切りファイル名切り出し変数
        for(int i = 0; g_OldFiles.Next(Name); i++) {
            //拡張子があればそのアドレスを、なければ0をfoundに入れる
            WCHAR* found = Name.ToChar() + lstrlenW(Name.ToChar());
            while(found > Name.ToChar() && *found != L'.')
                found--;
            if(found == Name.ToChar())
                found = 0;
            //置換して、新しいファイル名リスト(g_NewFiles)をコンマ(',')区切りで作成
            g_NewFiles = g_NewFiles + L"\"";        //引用符括り
            g_NewFiles = g_NewFiles + ToRep;        //変更後ファイル名(拡張子無し)
            wsprintfW(num, L"(%03d)", i);            //'0'でサプレスされた連番
            g_NewFiles = g_NewFiles + num;            //変更後ファイル名
            if(found)                                //拡張子があるファイル
                g_NewFiles = g_NewFiles + found;    //「'.'以下拡張子」を付加
            g_NewFiles = g_NewFiles + L"\",";        //引用符括りとコンマ区切り
        }
        ListFiles(m_hWnd, IDC_LISTBOX, &g_NewFiles);
        //「変換実行」ボタンを有効にする
        EnableWindow(GetDlgItem(m_hWnd, IDOK), TRUE);
        return TRUE;;
    }

//解説:これもコメントが多いのであまり説明する必要がないかもしれません。「文字列置換」との違いは「検索文字列」との一致が不要であること、連番を文字列で作成し、拡張子の前に置くこと、でしょうか?
}

 

さて、これでRenamerの解説はおしまい、となるところでしたが、C#で作っているReplacerのテストをしていて、LogファイルがCSTRのToFile関数を使っているためにUTF-16(余り使われないですよね?)です出力されていることが分かりました。ということで、CSTRのToSJISFile関数とToUTF8File関数を使って書き換えました。変更箇所は赤字で書いておきます。

 

【USER.h】

///////////////////////////////////////
//外部関数 RenameFile
//g_OrgFilesのカンマ区切りファイル名を
//g_NewFilesのカンマ区切りファイル名に
//変更する
//戻り値:一つでも失敗があればFALSE)
//"RenameLog(MM-DD-YYYY-HH-MM).log"と
//いうログファイルをプログラムの下の
//RemaneLogフォールダーに残す。
///////////////////////////////////////
bool RenameFiles(bool flag) {

    bool result = TRUE;
    //本日のローカル日付をログファイル(log)のfilename文字列に入れる
    CSTR log = L"";
    SYSTEMTIME st;
    GetLocalTime(&st);
    WCHAR fn[36];
    wsprintfW(fn, L"RenameLog(%02d-%02d-%04d-%02d-%02d-%02d).log", st.wMonth, st.wDay, st.wYear, st.wHour, st.wMinute, st.wSecond);
    //ファイル名変更実行
    g_OldFiles = g_OrgFiles;        //g_OrgFilesのデータを変更しないようにコピーのg_OldFilesを使う
    CSTR filename, buff1, buff2;
    while(g_OldFiles.Next(filename)) {
        buff1 = g_Path + L"\\";
        buff1 = buff1 + filename;
        g_NewFiles.Next(filename);    //最終的にg_NewFilesのデータは空になる
        buff2 = g_Path + L"\\";
        buff2 = buff2 + filename;
        if(MoveFileW(buff1.ToChar(), buff2.ToChar())) {    //'\"'で括るとエラーになるので注意
            log = log + L"\"" + buff1 + L"\",\"" + buff2 + L"\"\r\n";
        }
        else {
            result = FALSE;
        }
    }
    //ログを保存する(後にバッチファイルとして使える)
    filename = g_LogPath + fn;
    //flagがTRUEの場合SJIS、FALSEの場合UTF-8で保存
    if(flag)
        log.ToSJISFile(filename.ToChar());
    else    //UTF-8ファイルへの書き込み(FALSEはBOM無のフラグ)
        log.ToUTF8File(filename.ToChar(), FALSE);

    return result;
}
 

【RenamerProc.h】

//////////////////////////////
//SUBダイアログの関数の定義
//コントロール関数
//////////////////////////////
(省略)

bool SUBDLG::OnOk() {

    if(MessageBoxW(m_hWnd, L"RenameLogフォールダーにShift-JISで(はい)\r\nまたはUTF-8で(いいえ)\r\nログファイルを記録します。",
                    L"ログファイル形式確認", MB_YESNO | MB_ICONQUESTION) == IDYES) {
        if(RenameFiles(TRUE))
            MessageBoxW(m_hWnd, L"RenameLogフォールダーにログファイルをSJISで記録しました。", L"ログ保存確認", MB_OK | MB_ICONINFORMATION);
        else
            MessageBoxW(m_hWnd, L"ファイル名変更に失敗しました。", L"エラー", MB_OK | MB_ICONERROR);
    }
    else {
        if(RenameFiles(FALSE))
            MessageBoxW(m_hWnd, L"RenameLogフォールダーにログファイルをUTF-8で記録しました。", L"ログ保存確認", MB_OK | MB_ICONINFORMATION);
        else
            MessageBoxW(m_hWnd, L"ファイル名変更に失敗しました。", L"エラー", MB_OK | MB_ICONERROR);
    }

    EndModal(TRUE);
    return TRUE;
}
 

これでRenamerの解説はおしまいです。この解説を書き、Replacerを作りながら、

(1)Renamerのログは毎回"RenameLog(MM-DD-YYYY-HH-MM).log"というファイル名でログを残すべきか?

(2)新たにログファイルを書く際に過去ログをパックしてはどうか?

(3)てか、いっそログファイルは一本にして過去ログをズラズラ連ねてはどうか?

等々考えさせられましたが、どれも一長一短あり、元々「一括ファイル名変換」自体があまり頻繁にやる処理ではないし、そういうことをやったならば「その時にReplacer処理を同時にやらないと忘れてしまうし、そもそもやるだけの価値がないことになる」ので、悩んだ結果矢張り「何時、どういうファイル名変換をやったか」が正確に残る現在の形を維持し、一定期間見直さなければもう捨てるしかない、というのが一番効率的だと結論付けましたが、

 

何か?