none
TextBoxのタブを入力したときの表示サイズの変更 RRS feed

  • 質問

  • お世話になっております。WPFのTextBoxコントロールについて質問があります。

    TextBox.AcceptTab = True にすると、タブキーを押したときにタブ文字が入力できますが、

    その際デフォルトでは全角スペース4文字(半角では8文字)として表示されます。

    これを、半角4文字にしたいのですが、どの様にすれば良いかが調べてみても分かりませんでした。

    ご存知の方、ご教授ください。

    よろしくお願い致します。

    2012年5月13日 13:14

回答

  • TextBoxのTABの幅は、TAB以外の入力済みの文字数で変動するので厄介です。
    TAB文字が常時固定幅でいいならTABだけフォントの幅を変えればなんとかできます。

    TABを半角スペースの個数で代用する方法だとスペースとTABとが区別つかなくなりますが、この方法だと条件付ですがTABを維持することができます。

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        
        <TextBox AcceptsTab="true" AcceptsReturn="true"
                 xmlns:app="clr-namespace:WpfApplication1"
                 app:DummyTABTextBox.TabReplace="true" >
            <TextBox.Text>
                <Binding Path="Text" ElementName="txb"
                         UpdateSourceTrigger="PropertyChanged" >
                    <Binding.Converter>
                        <app:DummyTABTextBox 
                            xmlns:app="clr-namespace:WpfApplication1"/>
                    </Binding.Converter>
                </Binding>
            </TextBox.Text>
        </TextBox>
    
        <TextBox x:Name="txb" Grid.Row="1" 
                 AcceptsTab="true" AcceptsReturn="true"
                 FontFamily="MS Gothic"  
                 Text="ab&#9;cd"/>
    </Grid>
    /// <summary>TextBoxのTABの文字幅を変更する </summary>
    class DummyTABTextBox : IValueConverter
    {
        #region "添付ビヘイビア"
    
        public static bool? GetTabReplace(DependencyObject obj)
        {
            return (bool?)obj.GetValue(TabReplaceProperty);
        }
    
        public static void SetTabReplace(DependencyObject obj, bool? value)
        {
            obj.SetValue(TabReplaceProperty, value);
        }
    
        public static readonly DependencyProperty TabReplaceProperty
            = DependencyProperty.RegisterAttached
                ("TabReplace"
                , typeof(bool?)
                , typeof(DummyTABTextBox)
                , new UIPropertyMetadata(default(bool?), OnTabReplaceChanged));
    
        private static void OnTabReplaceChanged(DependencyObject dpo, DependencyPropertyChangedEventArgs e)
        {
            TextBox target = dpo as TextBox ;
            if (target != null)
            {
                bool? newValue = (bool?)e.NewValue;
                bool? oldValue = (bool?)e.OldValue;
    
                if (oldValue ?? false)
                {
                    Reset(target);
                }
    
                if (newValue ?? false)
                {
                    Set(target);
                }
            }
        }
    
        /// <summary>TABの入れ替え処理の初期化</summary>
        /// <param name="target"></param>
        public static void Set(TextBox target)
        {
            target.PreviewKeyDown += new KeyEventHandler(target_PreviewKeyDown);
            DataObject.AddPastingHandler(target, target_Pasting);
            DataObject.AddCopyingHandler(target, target_Copying);
            target.Text = target.Text.Replace('\t', TABDUMMY);
    
            //DummyTABというフォント名を持つフォントファイルをコンテンツとして持っていること
            //優先度が高いDummyTABは1文字だけがグリフが登録されていて、残りの文字はMS Gothicが代替フォントになる
            target.FontFamily = new FontFamily(new Uri("pack://application:,,,/"), "./#DummyTAB ,MS Gothic");
        }
        /// <summary>TABの入れ替え処理の無効化</summary>
        /// <param name="target"></param>
        public static void Reset(TextBox target)
        {
            target.PreviewKeyDown -= new KeyEventHandler(target_PreviewKeyDown);
            DataObject.RemovePastingHandler(target, target_Pasting);
            DataObject.RemoveCopyingHandler(target, target_Copying);
            target.Text = target.Text.Replace(TABDUMMY, '\t');
            target.FontFamily = new FontFamily("MS Gothic");
        }
        #endregion
    
        #region "TAB入力時に文字コード入れ替え"
    
        /// <summary>TABの代わりにする仮の文字</summary>
        /// <remarks>通常では使われないと思われる文字コードを指定する</remarks>
        public const char TABDUMMY = (char)0x11;
    
        private static void target_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Tab)
            {
                e.Handled = true;
                var txb = (TextBox)sender;
                txb.SelectedText = TABDUMMY.ToString();
                txb.SelectionStart = txb.SelectionStart + 1;
            }
        }
    
        private static void target_Pasting(object sender, DataObjectPastingEventArgs e)
        {
            if (!e.CommandCancelled)
            {
                e.CancelCommand();
                var txb=(TextBox)sender;
                txb.SelectedText = ((string)e.DataObject.GetData(DataFormats.Text)).Replace('\t', TABDUMMY);
            }
        }
    
        private static void target_Copying(object sender, DataObjectCopyingEventArgs e)
        {
            if (!e.CommandCancelled)
            {
                e.CancelCommand();
                var txb=(TextBox)sender;
                Clipboard.SetText(txb.SelectedText.Replace(TABDUMMY, '\t'));
            }
        }
        #endregion
    
        #region IValueConverter メンバ
    
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value == null)
            {
                return null;
            }
            else
            {
                return value.ToString().Replace('\t', TABDUMMY);
            }
        }
    
        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value == null)
            {
                return null;
            }
            else
            {
                return value.ToString().Replace(TABDUMMY, '\t');
            }
        }
        #endregion
    }

    DummyTAB.TTFファイル -> ZIPファイル ->base64したもの

    UEsDBBQAAAAIADV4rkAvkZyTmwIAAGgFAAAMAAAARHVtbXlUQUIudHRm5VTPaxNBFP5md5NYqTYRbxbcxqIoNt1sWrUliC1tCGixYIyChXZp0jTQTUJ+lLYnvXnzJnipnjzklpzEix6V9igiUvDiTepBFESU9dvJRKhK/4G+4e37vm/evPcShoEAcAR3oWM2lcrMOKc3jwKBa1T7r98YtrPjT98B4jb5wqLrVNCnvyB/RZ9cXK2biGr3Ab0XgFFwahXPg87zU+SHCivrS9NbzST3+8kzy3kn993+GCfeoV9YpmCMpe8A4jj54LJbX7s6gCj5M/LwSnnRQYYLepj8sOusVZDGGPcvkZslx81/uDV/nvsVapuVcq3+ZPDie/b3820IUObB2psf833JbzhpwLett7PT3Si72VrTnxcaOiYAqcyls5LKSnvMkIrRPSO/QmpXEOtkpLPiKwLceqRtkz/oRPEJCdFDFUEdyvZWn12azuElIojoCS+BOa35T44JIVYRlfJNoDMldOX9KjdJJiQ3YDIaauqI53W+3pduVRGkQ2FN/R41n+p+EEzeZZxTC9D+3A4dIcRaAsPJdsjArt0KBnaSbV0jREv35YAvt0NB8TPZFr6eiAxEziQip6Ii/Hl7W2v+yka1JdB68VD9n4L355jCvBA4q7COQWwobDCnpXAAYewoHKTuMVMYPRDUobDG+icU1pGGpbDBnMcKB2DiucJB6ruYQgMu1zoymMAkMNVw3fXMBNEM8sih6CeQ5HPFBmMKZZRQl7GKAlNM2IjBYhyn/1VPqXGMYAijdBsWmc1C5VI9Va4W8qYds8xxs9uXMD4yNDpkW3Eb+42XZe8qaijKiUxYsjInkctkQr5aK5ZLpmXFY5ZlmftWQ/cN8O4hgf+ZQRfQhBZqlIqsGZeq8Dz1BtBeb1xe67wvfQvqnRn9DVBLAQIUABQAAAAIADV4rkAvkZyTmwIAAGgFAAAMAAAAAAAAAAAAIQAAAAAAAABEdW1teVRBQi50dGZQSwUGAAAAAAEAAQA6AAAAxQIAAAAA

    フォントファイルはコンテンツファイルとしてプロジェクトに追加して、出力ディレクトリにコピーします。

    動作説明:
    TABをそのままTextBoxに表示できないので、別の文字コードに置換。(*)
    置換する文字コードに対応するフォントのグリフを任意の幅になるようにしてDummyTABというフォントのファイルを用意しておく。
    実行時には置換された文字だけがDummyTABのグリフを参照するので任意の幅の空白が表示される。
    置換された文字以外はDummyTABに登録されていないので代替フォントとしてMS Gothicで表示される。

    (*)TextBox内のGlyphRunがTABだけ特殊になってるのでTAB自体の表示はできないみたい


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答としてマーク 山本春海 2012年6月8日 8:35
    2012年5月14日 11:01
  • サンプルでつけたファイルでは、TAB幅の変化がわかりやすくなるようにわざと幅の広いフォントになっています。
    ですので、じっさいには必要な幅のフォントを作ることになります。

    フォントファイルを作ってみたのは今回が初めてなので、正しい作り方とかは分かりません。
    私はフォントファイルをFontForgeというソフトで作りました。
    このソフトでDummyTab.TTFを開いて、文字コード0x11の幅を調整することになります。
    現在の幅が18518とかなってるので、適当に幅を狭くしてみてください。
    実際に使いたいフォントを開いてみて、スペース1文字の幅が実際にはどの程度なのかをみて、何倍かで比較すればいいのかもしれません。

    #FontFamilyMapが使えればScaleプロパティがあって実行時に文字幅変えられるんですが、FontFamilyMapのローカルなフォントの指定方法が見つからないですorz

    2012/06/08 追記:
    .Net Framework 3.5までは上記のフォント構成で出来ますが、4.0だと0x00~0x1Fまでのコントロールコードは表示できなくなってるみたいです。
    なので、置換文字はプライベート領域の0xE009とかにする必要があります。
    # .notdef(-1)という未定義用グリフが幅0で描画されるっぽい


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)


    • 回答の候補に設定 sasarara79 2012年6月6日 14:09
    • 回答の候補の設定解除 sasarara79 2012年6月6日 14:10
    • 回答の候補に設定 sasarara79 2012年6月6日 14:12
    • 回答としてマーク 山本春海 2012年6月8日 8:35
    • 編集済み gekkaMVP 2012年6月8日 14:19 .Net Frameworkのバージョンによる差異について追記
    2012年5月28日 13:07

すべての返信

  • 調べてみましたが、今のところ以下のようにするしか無さそうですね・・・

    tab size in the textbox
    http://social.msdn.microsoft.com/forums/en-US/wpf/thread/0d267009-5480-4314-8929-d4f8d8687cfd/


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

    2012年5月14日 0:55
    モデレータ
  • TextBoxのTABの幅は、TAB以外の入力済みの文字数で変動するので厄介です。
    TAB文字が常時固定幅でいいならTABだけフォントの幅を変えればなんとかできます。

    TABを半角スペースの個数で代用する方法だとスペースとTABとが区別つかなくなりますが、この方法だと条件付ですがTABを維持することができます。

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        
        <TextBox AcceptsTab="true" AcceptsReturn="true"
                 xmlns:app="clr-namespace:WpfApplication1"
                 app:DummyTABTextBox.TabReplace="true" >
            <TextBox.Text>
                <Binding Path="Text" ElementName="txb"
                         UpdateSourceTrigger="PropertyChanged" >
                    <Binding.Converter>
                        <app:DummyTABTextBox 
                            xmlns:app="clr-namespace:WpfApplication1"/>
                    </Binding.Converter>
                </Binding>
            </TextBox.Text>
        </TextBox>
    
        <TextBox x:Name="txb" Grid.Row="1" 
                 AcceptsTab="true" AcceptsReturn="true"
                 FontFamily="MS Gothic"  
                 Text="ab&#9;cd"/>
    </Grid>
    /// <summary>TextBoxのTABの文字幅を変更する </summary>
    class DummyTABTextBox : IValueConverter
    {
        #region "添付ビヘイビア"
    
        public static bool? GetTabReplace(DependencyObject obj)
        {
            return (bool?)obj.GetValue(TabReplaceProperty);
        }
    
        public static void SetTabReplace(DependencyObject obj, bool? value)
        {
            obj.SetValue(TabReplaceProperty, value);
        }
    
        public static readonly DependencyProperty TabReplaceProperty
            = DependencyProperty.RegisterAttached
                ("TabReplace"
                , typeof(bool?)
                , typeof(DummyTABTextBox)
                , new UIPropertyMetadata(default(bool?), OnTabReplaceChanged));
    
        private static void OnTabReplaceChanged(DependencyObject dpo, DependencyPropertyChangedEventArgs e)
        {
            TextBox target = dpo as TextBox ;
            if (target != null)
            {
                bool? newValue = (bool?)e.NewValue;
                bool? oldValue = (bool?)e.OldValue;
    
                if (oldValue ?? false)
                {
                    Reset(target);
                }
    
                if (newValue ?? false)
                {
                    Set(target);
                }
            }
        }
    
        /// <summary>TABの入れ替え処理の初期化</summary>
        /// <param name="target"></param>
        public static void Set(TextBox target)
        {
            target.PreviewKeyDown += new KeyEventHandler(target_PreviewKeyDown);
            DataObject.AddPastingHandler(target, target_Pasting);
            DataObject.AddCopyingHandler(target, target_Copying);
            target.Text = target.Text.Replace('\t', TABDUMMY);
    
            //DummyTABというフォント名を持つフォントファイルをコンテンツとして持っていること
            //優先度が高いDummyTABは1文字だけがグリフが登録されていて、残りの文字はMS Gothicが代替フォントになる
            target.FontFamily = new FontFamily(new Uri("pack://application:,,,/"), "./#DummyTAB ,MS Gothic");
        }
        /// <summary>TABの入れ替え処理の無効化</summary>
        /// <param name="target"></param>
        public static void Reset(TextBox target)
        {
            target.PreviewKeyDown -= new KeyEventHandler(target_PreviewKeyDown);
            DataObject.RemovePastingHandler(target, target_Pasting);
            DataObject.RemoveCopyingHandler(target, target_Copying);
            target.Text = target.Text.Replace(TABDUMMY, '\t');
            target.FontFamily = new FontFamily("MS Gothic");
        }
        #endregion
    
        #region "TAB入力時に文字コード入れ替え"
    
        /// <summary>TABの代わりにする仮の文字</summary>
        /// <remarks>通常では使われないと思われる文字コードを指定する</remarks>
        public const char TABDUMMY = (char)0x11;
    
        private static void target_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Tab)
            {
                e.Handled = true;
                var txb = (TextBox)sender;
                txb.SelectedText = TABDUMMY.ToString();
                txb.SelectionStart = txb.SelectionStart + 1;
            }
        }
    
        private static void target_Pasting(object sender, DataObjectPastingEventArgs e)
        {
            if (!e.CommandCancelled)
            {
                e.CancelCommand();
                var txb=(TextBox)sender;
                txb.SelectedText = ((string)e.DataObject.GetData(DataFormats.Text)).Replace('\t', TABDUMMY);
            }
        }
    
        private static void target_Copying(object sender, DataObjectCopyingEventArgs e)
        {
            if (!e.CommandCancelled)
            {
                e.CancelCommand();
                var txb=(TextBox)sender;
                Clipboard.SetText(txb.SelectedText.Replace(TABDUMMY, '\t'));
            }
        }
        #endregion
    
        #region IValueConverter メンバ
    
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value == null)
            {
                return null;
            }
            else
            {
                return value.ToString().Replace('\t', TABDUMMY);
            }
        }
    
        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value == null)
            {
                return null;
            }
            else
            {
                return value.ToString().Replace(TABDUMMY, '\t');
            }
        }
        #endregion
    }

    DummyTAB.TTFファイル -> ZIPファイル ->base64したもの

    UEsDBBQAAAAIADV4rkAvkZyTmwIAAGgFAAAMAAAARHVtbXlUQUIudHRm5VTPaxNBFP5md5NYqTYRbxbcxqIoNt1sWrUliC1tCGixYIyChXZp0jTQTUJ+lLYnvXnzJnipnjzklpzEix6V9igiUvDiTepBFESU9dvJRKhK/4G+4e37vm/evPcShoEAcAR3oWM2lcrMOKc3jwKBa1T7r98YtrPjT98B4jb5wqLrVNCnvyB/RZ9cXK2biGr3Ab0XgFFwahXPg87zU+SHCivrS9NbzST3+8kzy3kn993+GCfeoV9YpmCMpe8A4jj54LJbX7s6gCj5M/LwSnnRQYYLepj8sOusVZDGGPcvkZslx81/uDV/nvsVapuVcq3+ZPDie/b3820IUObB2psf833JbzhpwLett7PT3Si72VrTnxcaOiYAqcyls5LKSnvMkIrRPSO/QmpXEOtkpLPiKwLceqRtkz/oRPEJCdFDFUEdyvZWn12azuElIojoCS+BOa35T44JIVYRlfJNoDMldOX9KjdJJiQ3YDIaauqI53W+3pduVRGkQ2FN/R41n+p+EEzeZZxTC9D+3A4dIcRaAsPJdsjArt0KBnaSbV0jREv35YAvt0NB8TPZFr6eiAxEziQip6Ii/Hl7W2v+yka1JdB68VD9n4L355jCvBA4q7COQWwobDCnpXAAYewoHKTuMVMYPRDUobDG+icU1pGGpbDBnMcKB2DiucJB6ruYQgMu1zoymMAkMNVw3fXMBNEM8sih6CeQ5HPFBmMKZZRQl7GKAlNM2IjBYhyn/1VPqXGMYAijdBsWmc1C5VI9Va4W8qYds8xxs9uXMD4yNDpkW3Eb+42XZe8qaijKiUxYsjInkctkQr5aK5ZLpmXFY5ZlmftWQ/cN8O4hgf+ZQRfQhBZqlIqsGZeq8Dz1BtBeb1xe67wvfQvqnRn9DVBLAQIUABQAAAAIADV4rkAvkZyTmwIAAGgFAAAMAAAAAAAAAAAAIQAAAAAAAABEdW1teVRBQi50dGZQSwUGAAAAAAEAAQA6AAAAxQIAAAAA

    フォントファイルはコンテンツファイルとしてプロジェクトに追加して、出力ディレクトリにコピーします。

    動作説明:
    TABをそのままTextBoxに表示できないので、別の文字コードに置換。(*)
    置換する文字コードに対応するフォントのグリフを任意の幅になるようにしてDummyTABというフォントのファイルを用意しておく。
    実行時には置換された文字だけがDummyTABのグリフを参照するので任意の幅の空白が表示される。
    置換された文字以外はDummyTABに登録されていないので代替フォントとしてMS Gothicで表示される。

    (*)TextBox内のGlyphRunがTABだけ特殊になってるのでTAB自体の表示はできないみたい


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答としてマーク 山本春海 2012年6月8日 8:35
    2012年5月14日 11:01
  • 返答が遅くなってしまい申し訳ありません。
    trapemiya様、gekka様、ご教授ありがとうございます。

    今回はタブ文字を維持したまま表示させたかったので、gekka様の方法を試してみました。

    タブ文字をダミー文字に置き換えて、特殊フォントで表示する自体はうまく動いているようですが、
    タブ幅がかなり広く(全角8文字分ぐらい)なってしまいました。

    そこで、再度質問があります。

    添付していただいた DummyTab.TTF ファイルが上手く適応するその他の制約(フォントサイズ等)
    はありますでしょうか?

    また、差し支えなければこのファイルをどのような手順で作成されたか教えていただけないでしょうか?

    2012年5月27日 14:18
  • サンプルでつけたファイルでは、TAB幅の変化がわかりやすくなるようにわざと幅の広いフォントになっています。
    ですので、じっさいには必要な幅のフォントを作ることになります。

    フォントファイルを作ってみたのは今回が初めてなので、正しい作り方とかは分かりません。
    私はフォントファイルをFontForgeというソフトで作りました。
    このソフトでDummyTab.TTFを開いて、文字コード0x11の幅を調整することになります。
    現在の幅が18518とかなってるので、適当に幅を狭くしてみてください。
    実際に使いたいフォントを開いてみて、スペース1文字の幅が実際にはどの程度なのかをみて、何倍かで比較すればいいのかもしれません。

    #FontFamilyMapが使えればScaleプロパティがあって実行時に文字幅変えられるんですが、FontFamilyMapのローカルなフォントの指定方法が見つからないですorz

    2012/06/08 追記:
    .Net Framework 3.5までは上記のフォント構成で出来ますが、4.0だと0x00~0x1Fまでのコントロールコードは表示できなくなってるみたいです。
    なので、置換文字はプライベート領域の0xE009とかにする必要があります。
    # .notdef(-1)という未定義用グリフが幅0で描画されるっぽい


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)


    • 回答の候補に設定 sasarara79 2012年6月6日 14:09
    • 回答の候補の設定解除 sasarara79 2012年6月6日 14:10
    • 回答の候補に設定 sasarara79 2012年6月6日 14:12
    • 回答としてマーク 山本春海 2012年6月8日 8:35
    • 編集済み gekkaMVP 2012年6月8日 14:19 .Net Frameworkのバージョンによる差異について追記
    2012年5月28日 13:07
  • 返答が遅くなってしまい申し訳ありません。

    gekka様、ありがとうございました。

    FontForgeを使用してタブ幅を調整することが出来ました。

    本当にありがとうございました。

    • 回答の候補に設定 sasarara79 2012年6月6日 14:12
    2012年6月6日 14:12
  • mipu さんと sasarara79 さんは同じ方なのでしょうか?

    ちなみに、スレッドを作成した人は回答としてマークができます。
    それ以外の人は回答の候補として設定が主になります。

    2012年6月6日 14:38