20200809のC#に関する記事は5件です。

【.NET】ボタンクリックで入力フォーム部品を規則的に増やす

環境

  • Visual Studio 2019
  • Windows Forms App(.NET Core)
  • Visual C#

はじめに

特定のボタンをクリックすると、入力フォームの部品を規則的に増やす方法について説明します。
ここでは下図のようなフォームで「プログラムの追加」ボタンをクリックすると、ラベル・テキストボックス・参照ボタンの一式を規則的に下に増やしたいという想定で話を進めます。
1_1.png

Form.cs[Design]の設定

まずはフォームのデザイン側の設定をしていきます。
デザイン側での設定は

  • 増加させる部品の親要素(コンテナやフォームウィンドウ)のスクロール設定
  • 要素(コンポーネント)の名前付け

の2点を行います。
部品を増やしていくと親要素の表示範囲を超えていくことが予想されますので、親要素にはスクロールできるように設定します。
私の場合、Panelというコンテナの中にこの一式を配置しているので、このPanelをスクロールできるようにします。
下のように親要素(コンポーネント)のプロパティの設定を変更して下さい。

コンポーネント プロパティ
増加させたい部品の親要素(コンテナまたはフォームウィンドウ) AutoScroll True

Nameは自分が分かりやすい識別子をつけてください。
ここでは下のようにNameを設定しています。

コンポーネント Name
1 「プログラムを追加」ボタン addProgButton
2 「プログラム名」ラベル prognameLabel1
3 「パス」ラベル pathLabel1
4 「プログラム名」テキストボックス prognameTextBox1
5 「パス」テキストボックス pathTextBox1
6 「参照」ボタン progRefeButton1
7 親要素のパネル allprogPanel

2.png

Form.csのコーディング

Form.csのコードは次のようになります。
上のName表と対応しているので合わせて見てください。

クリックイベントの処理の流れは以下の通りです。
クリック回数のカウント

コンポーネント一式の情報を設定
(Name属性、表示するテキスト、表示位置、既存コンポーネントと新規コンポーネントの距離)

設定した情報からコンポーネントを生成する

Form.cs
//「プログラムを追加」ボタンのクリックイベント
private void addProgButton_Click(object sender, EventArgs e)
  {
    numberofAPBClicks++; //クリック回数のカウント、同クラスのメンバ変数

    //prognameLabel
    string PNLABEL_NAME = $"prognameLabel{numberofAPBClicks+1}";
    string PNLABEL_TEXT = $"{numberofAPBClicks+1}. プログラム名";
    //既存ラベルと新規ラベルの距離
    const int PNLABEL_OFFSET = 100; 
    int[] PNLABEL_LOCATE = {
      prognameLabel1.Location.X, //X座標
      PNLABEL_OFFSET * numberofAPBClicks + allProgPanel.AutoScrollPosition.Y //Y座標
    };

    //pathLabel
    string PATHLABEL_NAME = $"pathLabel{numberofAPBClicks+1}";
    const string PATHLABEL_TEXT = "パス";
    const int PATHLABEL_OFFSET = 100;
    int[] PATHLABEL_LOCATE = {
      pathLabel1.Location.X, //X座標
      PATHLABEL_OFFSET * numberofAPBClicks + allProgPanel.AutoScrollPosition.Y //Y座標
    };

    //prognameTextBox
    string PNBOX_NAME = $"prognameTextBox{numberofAPBClicks+1}";
    const int PNBOX_OFFSET = 100; //テキストボックス間の距離(Y)
    const int PNBOX_FIRST_OFFSET = 28; //Panelと最上部のテキストボックスとの距離(Y)
    int[] PNBOX_LOCATE = { 
      prognameTextBox1.Location.X, //X座標
      PNBOX_OFFSET * numberofAPBClicks + PNBOX_FIRST_OFFSET + allProgPanel.AutoScrollPosition.Y //Y座標
    };
    int[] PNBOX_SIZE = {
      prognameTextBox1.Size.Width, //テキストボックス幅
      prognameTextBox1.Size.Height //テキストボックス高さ
    };

    //pathTextBox
    string PATHBOX_NAME = $"pathTextBox{numberofAPBClicks+1}";
    const int PATHBOX_OFFSET = 100;
    const int PATHBOX_FIRST_OFFSET = 28; //Panelと最上部のテキストボックスとの距離(Y)
    int[] PATHBOX_LOCATE = {
      pathTextBox1.Location.X, //X座標
      PATHBOX_OFFSET * numberofAPBClicks + PATHBOX_FIRST_OFFSET + allProgPanel.AutoScrollPosition.Y //Y座標
    };
    int[] PATHBOX_SIZE = {
      pathTextBox1.Size.Width, //テキストボックス幅
      pathTextBox1.Size.Height //テキストボックス高さ
    };

    //progRefeButton
    string PRBUTTON_NAME = $"progRefeButton{numberofAPBClicks+1}";
    const string PRBUTTON_TEXT = "参照";
    int PRBUTTON_TAG = numberofAPBClicks + 1;
    const int PRBUTTON_OFFSET = 100;
    const int PRBUTTON_FIRST_OFFSET = 28; 
    int[] PRBUTTON_LOCATE ={
      progRefeButton1.Location.X,
      PRBUTTON_OFFSET * numberofAPBClicks + PRBUTTON_FIRST_OFFSET + allProgPanel.AutoScrollPosition.Y
    };
    int[] PRBUTTON_SIZE ={
    progRefeButton1.Size.Width,
    progRefeButton1.Size.Height
    };

  createLabel(PNLABEL_NAME, PNLABEL_TEXT, PNLABEL_LOCATE);
  createLabel(PATHLABEL_NAME, PATHLABEL_TEXT, PATHLABEL_LOCATE);
  createTextBox(PNBOX_NAME, PNBOX_LOCATE, PNBOX_SIZE);
  createTextBox(PATHBOX_NAME, PATHBOX_LOCATE, PATHBOX_SIZE);
  createButton(PRBUTTON_NAME, PRBUTTON_TEXT, PRBUTTON_TAG, PRBUTTON_LOCATE, PRBUTTON_SIZE);
}


//ラベルを生成するメソッド
//引数にはName属性、表示されるテキスト、ラベルの表示位置を渡す
private void createLabel(string name, string text, int[] locate)
{
  //ラベルのインスタンス生成
  Label label = new Label();
  //プロパティ設定
  label.Name = name;
  label.Location = new Point(locate[0], locate[1]);
  label.Text = text;
  //ラベルをパネルの子要素として追加
  allProgPanel.Controls.Add(label);
}

//テキストボックスを生成するメソッド
//引数にはName属性、ボックスの表示位置、ボックスの大きさを渡す
private void createTextBox(string name, int[] locate, int[] size)
{
  TextBox textBox = new TextBox();
  textBox.Name = name;
  textBox.Location = new Point(locate[0], locate[1]);
  textBox.Size = new Size(size[0], size[1]);
  allProgPanel.Controls.Add(textBox);
  return textBox;
}

//ボタンを生成するメソッド
//引数にはName属性、ボタンに表示されるテキスト、ボタンの表示位置、ボタンの大きさを渡す
private void createButton(string name, string text, int[] locate, int[] size)
{
  Button button = new Button();
  button.Name = name;
  button.Text = text;
  button.Location = new Point(locate[0], locate[1]);
  button.Size = new Size(size[0], size[1]);
  button.Click += progRefeButton_Click;
  allProgPanel.Controls.Add(button);
}

補足

要素(コンポーネント)のName属性とは

要素のName属性について説明しておきます。
3.png
Name属性を指定することでコーディングをする際に、その指定したName値で要素のプロパティにアクセスしたり、各種イベント(クリックイベントなど)を検知したりできます。
なおデフォルトでは、ラベルの場合はlabel1、テキストボックスの場合はtextBox1などと設定されています。

例えば、label1.text = "Hello World!";でプログラム上でラベルのテキストを変更できますし、label1.BackColor = Blackなどとすることでラベルの背景色を変更できます。
アクセスできるプロパティや呼び出すことができるメソッド、設定できるイベントなどの詳細はこちら(Labelの場合)を確認してください

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity宴でUtage以下のアセットコードをカスタマイズしてしまった後に宴をアプデートするための作業ログ

Unity宴でUtage以下のアセットコードをカスタマイズしてしまった後に宴をアプデートするための作業ログ

この作業によって判断したいこと

デフォルトアセットのコードをカスタマイズしてしまうと、今後宴アップデートが走った際にカオスになる。
よって、
1. カスタマイズ箇所を把握した上で、アプデ後にカスタマイズ部分を追記するか
2. そもそも追記しなくでも良い方法を模索するか
の判断をするための作業でもある。

その他参考情報:宴3のアップデートガイド

https://madnesslabo.net/utage/?page_id=8551

事前調査

  • 現在の宴バージョンは3.8.3である。
  • 現時点で「Utage」以下においてカスタマイズしてしまっているコードを完全把握する。
  • 記憶では、UtageUguiTitleとUtageUguiMainGameをカスタマイズしているはず。

調査結果

  • UtageUguiTitleをカスタマイズしていた
  • シナリオを周回することを目的とした独自のシナリオチャート画面をscenariochartとして定義していた。
  • UtageUguiMainGameをカスタマイズしていた。
  • ゲームの進行状況を画面で把握してもらうためのプログレス画面をscenarioprogressとして定義していた。

カスタマイズ箇所を洗い出す

UtageUguiTitle

///

チャート画面
public UtageUguiScenarioChart scenariochart;

//「チャート」ボタンが押された
public virtual void OnTapScenarioChart()
{
Close();
scenariochart.Open(this);
}


UtageUguiMainGame

///

チャート画面
public UtageUguiScenarioChart scenariochart;

///

プログレス画面
public UtageUguiScenarioProgress scenarioprogress;

//「チャート」ボタンが押された
public virtual void OnTapScenarioChart()
{
Close();
scenariochart.Open(this);
}

//「プログレス」ボタンが押された
public virtual void OnTapScenarioProgress()
{
Close();
scenarioprogress.Open(this);
}

結果に漏れがないか調べる

  • VisualStudioの「複数ファイルで検索」機能を使用して、scenariochartとscenarioprogressを「Utage」以下で全検索してみる

検索結果

scenariochart

  • 先の調査結果以外ではヒットしなかったので問題なし

scenarioprogress

  • 先の調査結果以外ではヒットしなかったので問題なし

判断

  • すでに独自コードはオリジナルプロジェクトフォルダ内の「Script」以下でコーディングしていたので、「Utage」以下でのカスタマイズ量は微量であることが把握できた。よって仮にアップデートで上書きされても、上記を追記する事で稼働させることができるから、今後も「UtageUguiTitle」と「UtageUguiMainGame」を必要に応じてカスタマイズする方向で開発することにする。実は他にも色々といじりたいこともあるし。。。

アプデ手順

  • Utageフォルダを丸ごと削除して新規インポート
  • 該当箇所に必要なコードを追記
  • 表示動作検証

アップデート結果

  • 表示動作ともに問題なし
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ComboBox で、DataSource を設定しているのに SelectedIndex = 0 を設定すると例外が発生する

失敗
comboBox.DataSource = ItemList; //ItemListは、DBからSELECTしたエンティティクラスのリスト
comboBox.SelectedIndex = 0;

//上記のコードを実行すると、System.ArgumentOutOfRangeException が発生する。
//Unhandled exception. System.ArgumentOutOfRangeException: '0' の InvalidArgument=Value は 'SelectedIndex' に対して有効ではありません。 (Parameter 'value')
//Actual value was 0.

SelectedIndex を set する前に、CreateControl メソッドを実行すると動作した (インデックス0番の値が SelectedValue に set された。)

成功
comboBox.DataSource = ItemList;
comboBox.CreateControl();
comboBox.SelectedIndex = 0;

環境 C# (.net 5 / C #8.0)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Windowsのタスクバーでもネコ走らせてみた?

はじめに

RunCat

みなさまはMacのメニューバーでネコを飼うRunCatというアプリをご存知でしょうか?CPU負荷に合わせて走る速度の変わるネコをメニューバー上に表示するだけというしょうもないアプリですが、現在では世界累計45,000ダウンロードを突破し、多くのみなさまに可愛がってもらえる定番アプリとなりつつあります。はじめは悪戯心で作成したジョークアプリが、思いもよらず高評価をいただけており大変嬉しいです。

一方で、Windows版がほしいとの声もちらほら聞こえるようになり、Windows版の模倣アプリも作られつつあることを知りました。そこで、ちゃんと本家からWindows版も出したいなぁと思い立ったが吉日ということで、格安でThinkPadを仕入れてWindowsアプリ開発に初挑戦してみました。

成果物

RunCat for Windows
Windowsのタスクバー上に常駐し、CPU負荷に応じてネコが走るだけの最小限機能を搭載したアプリを開発しました。GitHub上にソースコードを公開しており、アプリそのもの(RunCat.exe)もここ経由でダウンロード可能にしています。→Kyome22/RunCat_for_windows

RunCat for Windows

初めてのWindowsマシン

  • ThinkPad E495(14-inchノート)
  • OS: Windows 10 Home 64
  • メモリ: 16GB
  • プロセッサ: Ryzen 5 3500U 2.1G 4C MB
  • SSD: 256GB
  • キーボード: US配列カスタマイズ
  • 価格: ¥77,550

メインマシンとして使うつもりはなく、小さいWindows向けアプリを開発できる程度にVisual Studioが動けばOKという気持ちでスペック決めをしました。このくらいのスペックのマシンが8万円弱で手に入ったのは上々ではないでしょうか。(調べてみたらMacBook Airだとメモリ8GB、SSDが256でも10万円オーバーなんですね。)

ただ、仕方がないことなのですが、メインマシンと同じUS配列ではあるものの、キー配置がかなり違っていて入力ミスが多いです。とりわけControlキーとS, Z, X, C, Vキーあたりの配置がかなり厄介者でMacで慣れた手つきでショートカットを入力すると、一つずれたコマンドを叩いてしまうんですよね。慣れるまで我慢...

初めてのVisual Studio開発

今回はVisual Studio 2019 (コミュニティ)を使いました。
Windows向けのアプリ開発プロジェクトの種類

  • Windows フォームアプリケーション
  • WPF: Windows Presentation Foundation アプリ
  • UWP: Universal Windows Platform

このような感じで何種類かあり、よく耳にする.NET Coreや.NET Frameworkなどフレームワークにより微妙に種類が違ったりするようですが、よくわからなかったので、タスクバーのアイコンについて検索した時に出てきたNotifyIconというキーワードを頼りに、タスクバーだけのアプリが作れるプロジェクト形式ということでWindows フォームアプリケーションを選択しました。

Visual Studioでのアプリ開発について、基本的にXcodeでのアプリ開発と流れは変わらないので、あまり難しくは感じませんでした。強いて言えば画像や色などのリソース管理について、XcodeのAssets CatalogとVisual StudioのProperties-Resourcesは使い勝手が結構異なるので、とっつきづらかったですね。ただ、Propertiesで管理することでResources.リソース名のようにリソースをコード上で扱うことができるようになるので、ここは便利ですね。(Android開発でのR.リソースキー名に似ている)

開発ハマりポイント

最初に挑戦するアプリとしてはRunCatはかなり特殊なところをついているので、案の定いくつかハマってしまいました。

なんか参考文献が古い!

c# NotifyIcon sizeとかc# cpu usageとかc# detect dark themeとかのキーワードで調べていくんですが、大体行き着く文献が2011年や2007年とかかなり古い記事ばかりなんですよね。おそらくWindows フォームアプリケーションが古い形式なのがいけないのでしょうが、信憑性がよくわからないし、今(Windows 10)でも通じる技術なのかもよくわからない感じで裏を取るのが結構大変でした。

icoってなんや

Windowsアプリではアイコンを扱う時はicoという拡張子のものを使うんですが、こいつが曲者で、一つのファイルで複数のサイズの画像を取り扱えるやつなんですね。Webだとファビコンの拡張子がicoなんですが、これを自作するのがmacだと厄介で、Preview.appで書き出す時にoptionキーを押しながら形式を選択しようとするとMicrosoft アイコンというのが選択できてicoで出力できるのですが、複数サイズに対応できるわけではありません。また、PhotoShopを使ってもプラグインを入れなければicoは取り扱えません。あまり編集/書き出し環境が整っていない形式がなぜ普及しているのかよくわかりません。

どう頑張っても16×16pxの制限を突破できない

上にあげた成果物のGIFアニメーションをみていただければ分かる通り、タスクバー上のネコの解像度はかなり悪いです。それもそのはず、たったの16×16pxしか与えられていないのですから、ネコの輪郭がかなり潰れてしまいました。一応環境設定でUIを125%に拡大するオプションをオンにすれば、32×32pxのリソースが採用されるようですが、それでもかなり狭いです。まず、正方形でなければならないというのがかなり苦しい。NotifyIconに表示できる画像はicoなので、自ずと正方形になるのですが、横幅をもう少しくれれば...ぐぬぬという気持ちです。macOS版の方は正方形の縛りがなく、Retinaディスプレイならば56×36px、それ以下の解像度のディスプレイでも28×18pxが使えるのでかなりましですね。

ダークテーマかどうかの取得方法が煩雑

昨今のダークモードの流行りを取り入れたのかはわかりませんが、最近のWindowsではダークテーマというのを環境設定で選択できるようです。しかし、昔からあった仕様ではなく、Windows フォームアプリケーションの方はフレームワークがあまりアップデートされていないため、現在のテーマがダークテーマかどうかを取得する簡単な方法はないようです。どっかの設定に書き込まれている情報を直接読み取って、現在どちらのテーマに属しているのか判断するというのが苦肉の策としてあるようです。
Win32アプリケーションでWindows 10のライト/ダークモードを検出する方法
C# Windows10のダークモードいいっすね!アプリケーションのダークモード設定を取得してみましょ!ヽ(^。^)ノ
ユーザー設定の変更をイベントで受け取る

Win32とUWPの違いが一見わからん

いろいろ実装を試しながら文献を調査していると、Microsoft.Win32由来のクラスやメソッドを使っている実装例と、UWP由来のクラスやメソッドを使っている実装例が、文献によって入り乱れていることに気付きました。「よっしゃ、これなら簡単に実装できそう」と思ったらUWP由来で今回は使えない...というのが何回もあって萎えました...

EventHandlerを直呼びするのどうやるの?

RunCatは5秒に一回CPU負荷を取得してネコの走る速さを更新するのですが、そこでTimerを使っています。しかし、SwiftのTimer.fire()のようにTimerに指定した処理を即時発火するメソッドが用意されていないので、直接処理をコールしたくなりました。そこで、Timerに登録するEventHandler用のメソッドは、引数に(object sender, EventArgs e)を持つので引数が用意できず直接叩けないじゃんという状態になったのですが、メソッド内でsendereも使っていないならば、適当に引数詰めてコールすれば良いようです。

private void ObserveCPUTick(object sender, EventArgs e)
{
    // 処理
}
// 直接叩く
ObserveCPUTick(null, EventArgs.Empty);

所感

まぁいろいろ書きましたが、現状のものはVisual Studioを初めて起動してからだいたい3日くらいで実装できたので、mac版のときの躓きに比べたらかなり楽な勝負でしたね(Storeでのリリースもないし)。特に、CPU負荷取得するのが簡単すぎて逆に驚きました。たった数行でこういうシステム情報が取ってこれるところがWindowsアプリの良さかもしれません。

最小限機能のみの状態で、まだまだ改善の余地はありますが、とりあえず成果物を出すことができて満足ですね。今後はオープンソースの強みを活かして、みなさまのIssueやプルリクを待ってみようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C# - DataGridViewのDataSourceをAllowUserToAddRows=trueで運用するとプログラムからデータを追加できなくなる?

DataGridViewのDataSourceをAllowUserToAddRows=trueで運用するときの振る舞い

DataGridView は、AllowUserToAddRows=true(※このプロパティはデフォルトでtrueになっている)で運用すると、BindingSourceで紐づけている内部データの末尾要素に、ユーザー編集用の1行分のデータ(以下「仮のデータ」と呼ぶことにする)を勝手に追加したり取り消し削除したりと、やりたい放題してくれちゃう仕様のようです。

そのため、BindingSourceAddメソッドで追加すると、「仮のデータ」の後ろにデータが追加されるため、内部的に要素数の不整合が発生するようです。

対策

ユーザーによる直接入力での行追加をさせないのであれば、AllowUserToAddRows=falseで使うのが一番手っ取り早い。
AllowUserToAddRows=trueで運用したいなら、以下のように、「仮のデータ」があるかどうかによって、挿入するインデックスを調整すれば、正しく追加できるようである。

ソースコード

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;


class MainForm : Form
{
    DataGridView dgv;
    BindingList<ExampleItem> items;
    BindingSource wrapper;

    // DataGridViewに表示する項目
    public class ExampleItem
    {
        // フィールドは表示されない。プロパティにする必要がある。
        public string FileName {get;set;}

        public ExampleItem()
        {
            Console.WriteLine("ExampleItem() called.");
            FileName="xxx";
        }

        public static ExampleItem CreateItem(string fileName)
        {
            var item = new ExampleItem(){ FileName = fileName };
            Console.WriteLine(" by CreateItem.");
            return item;
        }
    }

    MainForm(string fileName)
    {
        items = new BindingList<ExampleItem>();

        Controls.Add(
            dgv = new DataGridView() {
                Dock = DockStyle.Fill,
                //AllowUserToAddRows = false,
                //AutoGenerateColumns = false,
                AllowDrop = true,
            }
        );
        wrapper = new BindingSource();
        wrapper.AddingNew       += (s,e)=>{Console.WriteLine("[BindingSource Event] AddingNew");};
        wrapper.BindingComplete += (s,e)=>{Console.WriteLine("[BindingSource Event] BindingComplete");};
        wrapper.DataSource = items;
        dgv.DataSource = wrapper;

        dgv.DragEnter += Control_DragEnter;
        dgv.DragDrop += Control_DragDrop;

        if ( fileName != null ) {
            RegisterItem(fileName);
        }
    }

    void Control_DragEnter(Object sender, DragEventArgs e)
    {
        if (e.Data.GetDataPresent(DataFormats.FileDrop)) {
            e.Effect = DragDropEffects.Copy;
        }
        else {
            e.Effect = DragDropEffects.None;
        }
    }

    void Control_DragDrop(Object sender, DragEventArgs e)
    {
        Console.WriteLine("[Event] DragDrop");
        var fileNames = (string[])e.Data.GetData(DataFormats.FileDrop, false);
        foreach ( var s in fileNames ) {
            RegisterItem(s);
        }
    }

    void RegisterItem(string filePath)
    {
        var item = ExampleItem.CreateItem(filePath);
        if ( item != null ) {

            Console.Write("DataGridView's Row Count: ");
            Console.WriteLine(dgv.Rows.Count);
            Console.Write("Internal data's Row Count: ");
            Console.WriteLine(items.Count);

            Console.WriteLine("Add...");
            if ( dgv.Rows.Count >= items.Count && items.Count >= 1 ) { // 対策コード
                wrapper.Insert(items.Count-1, item);                   // 対策コード
            }                                                          // 対策コード
            else {                                                     // 対策コード
                wrapper.Add(item);
            }                                                          // 対策コード
            Console.WriteLine("completed");

            Console.Write("DataGridView's Row Count: ");
            Console.WriteLine(dgv.Rows.Count);
            Console.Write("Internal data's Row Count: ");
            Console.WriteLine(items.Count);
        }
    }

    [STAThread]
    static void Main(string[] args)
    {
        Application.Run(new MainForm((args.Length==1)?args[0]:null));
    }
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む