ファイル選択ダイアログボックスによる参照機能付きテキストボックス

中邑です。 

とりあえず始めたブログにとりあえず記事を書いてとりあえず継続します。


今回はC#のネタです。


ファイルやフォルダのパスを指定する場面でよく見かけるコイツ
f:id:Ponkotsu4989:20210221203339p:plain
ソースコードを書きたいと思います。

何て読んだらいいかわからないので、とりあえず(4回目)
「ファイル選択ダイアログボックスによる参照機能付きテキストボックス」と命名します。

これくらいのソースコードはネットに出回っていると思っていたのですが、
調べても出てこないので(私の調査不足かもしれません)、
この記事を書こうと思ったわけでございます。


クラス名はとりあえず(5回目)「CustomTextBox」とします。
なんか違うような気もするのですが、いい名前が思いつかないので、
とりあえず(6回目)これでいきましょう。

XAML

<UserControl
    x:Class="UserControls.CustomTextBox"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    d:DesignHeight="50"
    d:DesignWidth="500"
    mc:Ignorable="d">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="auto" />
        </Grid.ColumnDefinitions>
        <TextBlock Grid.ColumnSpan="2" Text="{Binding Explain}" />
        <TextBox
            Grid.Row="1"
            VerticalContentAlignment="Center"
            AllowDrop="True"
            Drop="TextBox_Drop"
            PreviewDragOver="TextBox_PreviewDragOver"
            Text="{Binding Text}" />
        <Button
            Grid.Row="1"
            Grid.Column="1"
            Click="OpenButton_Click">
            参照
        </Button>
    </Grid>
</UserControl>

C#

using Microsoft.Win32;
using Microsoft.WindowsAPICodePack.Dialogs;
using System;
using System.ComponentModel;
using System.IO;
using System.Windows;
using System.Windows.Controls;

namespace UserControls
{
    /// <summary>
    /// ダイアログボックスによる参照とドラッグアンドドロップによる入力が可能なテキストボックスを提供します。
    /// </summary>
    public partial class CustomTextBox : UserControl
    {
        private OpenFileDialog Dialog;
        private CommonOpenFileDialog FolderDialog;
        private CustomTextBoxViewModel ViewModel;
        private string ErrorMessage;

        /// <summary>
        /// 新しい<see cref="CustomTextBox"/>クラスのインスタンスを初期化します。
        /// </summary>
        public CustomTextBox()
        {
            InitializeComponent();

            Dialog = new OpenFileDialog
            {
                Filter = ""
            };

            FolderDialog = new CommonOpenFileDialog();

            ViewModel = new CustomTextBoxViewModel
            {
                Text = "",
                Explain = ""
            };

            DataContext = ViewModel;

            IsFolderDialog = false;
        }

        /// <summary>
        /// ファイル選択ダイアログのフィルタを取得または設定します。
        /// 既定値は<see cref="string.Empty"/>です。
        /// </summary>
        public string Filter
        {
            get
            {
                return Dialog.Filter;
            }
            set
            {
                Dialog.Filter = value;
            }
        }

        /// <summary>
        /// テキストボックス内の文字列を取得または設定します。
        /// </summary>
        public string Text
        {
            get
            {
                return ViewModel.Text;
            }
            set
            {
                ViewModel.Text = value;
            }
        }

        /// <summary>
        /// テキストボックスに入力する値の説明を取得または設定します。
        /// 既定値は<see cref="string.Empty"/>です。
        /// </summary>
        public string Explain
        {
            get
            {
                return ViewModel.Explain;
            }
            set
            {
                ViewModel.Explain = value;
            }
        }

        /// <summary>
        /// フォルダ選択ダイアログとして使用するかどうかを取得または設定します。
        /// 既定値は<see langword="false"/>です。
        /// </summary>
        public bool IsFolderDialog
        {
            get
            {
                return FolderDialog.IsFolderPicker;
            }
            set
            {
                FolderDialog.IsFolderPicker = value;
            }
        }

        /// <summary>
        /// テキストボックスに入力した値に不正があった場合、メッセージボックスを開きエラーメッセージを表示するかどうかを取得または設定します。
        /// 既定値は<see langword="true"/>です。
        /// </summary>
        public bool IsShowingError { get; set; } = true;

        /// <summary>
        /// ファイル選択ダイアログボックスを開き、選択したファイルのパスをテキストボックスへ代入する
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="routed"></param>
        private void OpenButton_Click(object sender, RoutedEventArgs routed)
        {
            if (IsFolderDialog)
            {

                if (FolderDialog.ShowDialog() == CommonFileDialogResult.Ok)
                {
                    SetPathToTextBox(FolderDialog.FileName);
                }
                return;
            }
            if (Dialog.ShowDialog() == true)
            {
                Dialog.Filter = Filter;
                SetPathToTextBox(Dialog.FileName);
            }
        }

        private void TextBox_PreviewDragOver(object sender, DragEventArgs drag)
        {
            if (drag.Data.GetDataPresent(DataFormats.FileDrop, true))
            {
                drag.Effects = DragDropEffects.Copy;
            }
            else
            {
                drag.Effects = DragDropEffects.None;
            }
            drag.Handled = true;
        }

        private void TextBox_Drop(object sender, DragEventArgs drag)
        {
            var dropFiles = drag.Data.GetData(DataFormats.FileDrop, true) as string[];

            if (dropFiles != null)
            {
                SetPathToTextBox(dropFiles[0]);
            }
        }

        /// <summary>
        /// テキストボックスにパスを代入します。
        /// </summary>
        /// <param name="path"></param>
        private void SetPathToTextBox(string path)
        {
            Text = path;
            ViewModel.Text = path;
        }

        /// <summary>
        /// メッセージボックスを開き、エラーメッセージを表示します。
        /// </summary>
        private void ShowError()
        {
            MessageBox.Show(ErrorMessage, "エラー", MessageBoxButton.OK, MessageBoxImage.Error);
            ErrorMessage = "";
        }

        /// <summary>
        /// テキストボックスに入力されたパスまたはディレクトリの存在するか調べます。
        /// </summary>
        /// <param name="createNewDirectory">
        /// <see langword="true"/>の場合、入力したディレクトリが存在しない場合に新しいディレクトリを作成します。
        /// </param>
        /// <returns>
        /// テキストボックスに入力されたパスまたはディレクトリが存在する場合は<see langword="true"/>を返します。
        /// 存在しない場合や、長さが0の文字列の場合は<see langword="false"/>を返します。
        /// </returns>
        public bool CheckPathOrDirectory(bool createNewDirectory)
        {
            bool result = true;

            if (IsFolderDialog)
            {
                result = CheckDirectory(createNewDirectory);
            }
            else
            {
                result = CheckPath();
            }

            if (!result && IsShowingError)
            {
                ShowError();
            }

            return result;
        }

        /// <summary>
        /// テキストボックスに入力されたパスの存在の有無を調べます。
        /// </summary>
        /// <returns>
        /// テキストボックスに入力されたパスが存在する場合は<see langword="true"/>を返します。
        /// 存在しない場合や、長さが0の文字列の場合は<see langword="false"/>を返します。
        /// </returns>
        private bool CheckPath()
        {
            bool result = true;

            if (string.IsNullOrEmpty(Text))
            {
                ErrorMessage = "パスを入力してください";
                result = false;
            }
            else if (!File.Exists(Text))
            {
                ErrorMessage = $"{Text}が見つかりませんでした。";
                result = false;
            }

            return result;
        }

        private bool CheckDirectory(bool createNewDirectory = true)
        {
            if (!Directory.Exists(Text))
            {
                if (createNewDirectory)
                {
                    try
                    {
                        Directory.CreateDirectory(Text);
                        return CheckDirectory();
                    }
                    catch (Exception ex)
                    {
                        ErrorMessage = ex.Message;
                        return false;
                    }

                }
                else
                {
                    ErrorMessage = $"{Text}が見つかりませんでした。";
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// データバインディング用の内部クラスです。
        /// </summary>
        class CustomTextBoxViewModel : INotifyPropertyChanged
        {
            private string text;
            private string explain;
            public event PropertyChangedEventHandler PropertyChanged;

            public string Text
            {
                get
                {
                    return text;
                }
                set
                {
                    text = value;
                    NotifyPropertyChanged("Text");
                }
            }

            public string Explain
            {
                get
                {
                    return explain;
                }
                set
                {
                    explain = value;
                    NotifyPropertyChanged("Explain");
                }
            }

            /// <summary>
            /// プロパティの値が変更されたことを通知します。
            /// </summary>
            /// <param name="propertyName">プロパティ名</param>
            private void NotifyPropertyChanged(string propertyName)
            {
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                }
            }
        }

    }

}

ちなみに、このクラスを利用するにはMicrosoft.WindowsAPICodePack-Shellをインストールする必要があります。
インストール方法はこれでも読め

最後に

フォントファミリーやフォントサイズが変更できなかったり、
パスの入力を禁止にできないなど、まだ発展途上ではありますが
これからも随時修正する予定です。