akuruhinode's blog

pythonやC#を中心に興味を持った内容について調べています。

WPF クリック時にエフェクトを表示するボタンを作成する

はじめに

UIデザインにおけるボタンは画面という平面上でどうやってクリック、タッチ感をユーザにフィードバックするのかを考える必要があります。

特にフラットデザインのような影を付けないシンプルなデザインの場合は、この操作感を出すのが難しいと考えています。

そこで、ここではクリックしたときに周囲にエフェクトを表示するボタンを作成してみます。

作成したボタン

今回は以下のようなボタンを作成してみました。


WPF スタイル エフェクトを表示するボタン1

特徴としては以下の通りです。
1. 基本的には影を付けず単色を使ったフラットなデザイン。
2. マウスホバー時に色を薄くすることで、クリックできそうな印象を与える。
3. クリック時にボタンの周りにエフェクトを表示することで、クリックしたことをフィードバックする。

ソースコード

ここではカスタムコントロールとして作成します。カスタムコントロールといっても特別なことはしていないので、単純にWindow.Resourcesにスタイル部分だけを定義することでも作成できます。

プロジェクトへの追加

プロジェクトの追加から「カスタムコントロール」を選択し、CustomButton1.csを追加します。

CustomButton1.cs

作成されたCustomButton1.csを開き、以下のコメントの通り継承元をButtonに変更します。

namespace WpfApp1
{
    public class CustomButton1 : Button // 継承元をControlからButtonに変更する
    {
        static CustomButton1()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomButton1), new FrameworkPropertyMetadata(typeof(CustomButton1)));
        }
    }


その後Generic.xamlを開き以下のように編集します。

Generic.xaml

<?xml version="1.0" encoding="Shift_JIS" ?>
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApp1">

    <Style TargetType="{x:Type local:CustomButton1}">
        <Setter Property="Cursor" Value="Hand" />
        <Setter Property="Background" Value="#00b894" />
        <Setter Property="Foreground" Value="#ffffff" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:CustomButton1}">
                    <Grid>
                        <!--  ボタンの外側  -->
                        <Border
                            x:Name="backborder"
                            Background="LightGray"
                            CornerRadius="4"
                            Opacity="0">
                            <!--  ぼかし効果を付ける  -->
                            <Border.Effect>
                                <BlurEffect Radius="2" />
                            </Border.Effect>
                        </Border>
                        <!--  ボタンのスタイル  -->
                        <Border
                            x:Name="buttonborder"
                            Margin="4"
                            Background="{TemplateBinding Background}"
                            CornerRadius="4">
                            <TextBlock
                                HorizontalAlignment="Center"
                                VerticalAlignment="Center"
                                FontWeight="Bold"
                                Foreground="{TemplateBinding Foreground}">
                                <ContentPresenter />
                            </TextBlock>
                        </Border>
                        <!--  ボタン内側  -->
                        <Border
                            x:Name="foreborder"
                            Margin="4"
                            Background="White"
                            CornerRadius="4"
                            Opacity="0" />
                    </Grid>
                    <ControlTemplate.Resources>               
                        <!--  クリック時の外側のアニメーション  -->
                        <Storyboard x:Key="BackColorAnimation">
                            <DoubleAnimation
                                BeginTime="0:0:0.05"
                                Storyboard.TargetName="backborder"
                                Storyboard.TargetProperty="Opacity"
                                From="0"
                                To="0.7"
                                Duration="0:0:0.15" />
                            <DoubleAnimation
                                BeginTime="0:0:0.15"
                                Storyboard.TargetName="backborder"
                                Storyboard.TargetProperty="Opacity"
                                From="0.7"
                                To="0"
                                Duration="0:0:0.55" />
                        </Storyboard>
                        <!--  クリック時の内側のアニメーション  -->
                        <Storyboard x:Key="ForeColorAnimation">
                            <DoubleAnimation
                                Storyboard.TargetName="foreborder"
                                Storyboard.TargetProperty="Opacity"
                                From="0"
                                To="0.4"
                                Duration="0:0:0.15" />
                            <DoubleAnimation
                                BeginTime="0:0:0.15"
                                Storyboard.TargetName="foreborder"
                                Storyboard.TargetProperty="Opacity"
                                From="0.4"
                                To="0"
                                Duration="0:0:0.5" />
                        </Storyboard>
                    </ControlTemplate.Resources>
                    <ControlTemplate.Triggers>
                        <!--  クリック時のスタイル設定  -->
                        <Trigger Property="IsPressed" Value="True">
                            <Trigger.EnterActions>
                                <BeginStoryboard Storyboard="{StaticResource BackColorAnimation}" />
                                <BeginStoryboard Storyboard="{StaticResource ForeColorAnimation}" />
                            </Trigger.EnterActions>
                        </Trigger>
                        <!--  マウスホバー時のスタイル設定  -->
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter TargetName="buttonborder" Property="Opacity" Value="0.9" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

</ResourceDictionary>

MainWindow.xaml

<Window
    x:Class="WpfApp1.MainWindow"
    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:local="clr-namespace:WpfApp1"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Grid>
        <local:CustomButton1
            Width="100"
            Height="40"
            Content="BUTTON" />
    </Grid>
</Window>

解説

Generic.xamlで作成したスタイルについて解説します。

色の定義

以下のようにBackground、Foregroundを定義しておき、それを実際のボタンとして表示するBroderのBackground、TextBlockのForegroundにTemplateBinding します。
そうすることで利用時にBackground、Foregroundを変更できるようにしておきます。

<Style TargetType="{x:Type local:CustomButton1}">
    <Setter Property="Background" Value="#0984e3" />
    <Setter Property="Foreground" Value="#ffffff" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:CustomButton1}">
            ・・・省略・・・
                    <!--  ボタンのスタイル  -->
                    <Border
                    ・・・省略・・・
                        Background="{TemplateBinding Background}"
                    ・・・省略・・・
                        <TextBlock
                    ・・・省略・・・
                            Foreground="{TemplateBinding Foreground}">

アニメーション用のBorderの作成

ここでは以下のように、Grid内に3つのBorderを重ねています。上から、ボタンの外側、ボタン本体、ボタンの内側になります。

ボタンの外側と内側のOpacityをアニメーションで変化させることでクリック時のアニメーションを実現します。外側と内側のBorderはアニメーション未実行時は表示しないので、Opacityを0にしています。

ボタン本体、ボタンの内側のMarginを指定することで外側のBorderが若干はみ出すようにしています。このはみ出している部分が外側のエフェクトになります。
外側のBorderにBlurEffectを設定することで、少しだけ外側のBorderをぼかしています。

<ControlTemplate TargetType="{x:Type local:CustomButton1}">
    <Grid>
    <!--  ボタンの外側  -->
        <Border
            x:Name="backborder"
            Background="LightGray"
            CornerRadius="4"
            Opacity="0">
            <!--  ぼかし効果を付ける  -->
            <Border.Effect>
                <BlurEffect Radius="2" />
            </Border.Effect>
        </Border>
        <!--  ボタンのスタイル  -->
        <Border
            x:Name="buttonborder"
            Margin="4"
            Background="{TemplateBinding Background}"
            CornerRadius="4">
            <TextBlock
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                FontWeight="Bold"
                Foreground="{TemplateBinding Foreground}">
                <ContentPresenter />
            </TextBlock>
        </Border>
        <!--  ボタン内側  -->
        <Border
            x:Name="foreborder"
            Margin="4"
            Background="White"
            CornerRadius="4"
            Opacity="0" />
    </Grid>

アニメーションの作成

ControlTemplate.Resourcesにアニメーションの動作を定義します。
今回は、ボタン外側、内側のOpacityを滑らかに変化させたいので、DoubleAnimation
を利用します。外側用、内側用の2つのStoryboardを定義していますが、それぞれ少しだけ時間をずらしています。具体的には、わずかに外側のStoryboardを遅延させています。
こうすることでStoryboardとしてはシンプルですがエフェクトが内側から外側へ広がる感じを表現してみました。

<ControlTemplate.Resources>               
    <!--  クリック時の外側のアニメーション  -->
    <Storyboard x:Key="BackColorAnimation">
        <DoubleAnimation
            BeginTime="0:0:0.05"
            Storyboard.TargetName="backborder"
            Storyboard.TargetProperty="Opacity"
            From="0"
            To="0.7"
            Duration="0:0:0.15" />
        <DoubleAnimation
            BeginTime="0:0:0.15"
            Storyboard.TargetName="backborder"
            Storyboard.TargetProperty="Opacity"
            From="0.7"
            To="0"
            Duration="0:0:0.55" />
    </Storyboard>
    <!--  クリック時の内側のアニメーション  -->
    <Storyboard x:Key="ForeColorAnimation">
        <DoubleAnimation
            Storyboard.TargetName="foreborder"
            Storyboard.TargetProperty="Opacity"
            From="0"
            To="0.4"
            Duration="0:0:0.15" />
        <DoubleAnimation
            BeginTime="0:0:0.15"
            Storyboard.TargetName="foreborder"
            Storyboard.TargetProperty="Opacity"
            From="0.4"
            To="0"
            Duration="0:0:0.5" />
    </Storyboard>
</ControlTemplate.Resources>

クリック時のアニメーション、マウスオーバー時のスタイルを設定

ControlTemplate.Triggersでトリガーを設定して、動作させるアニメーションのStoryboardを指定します。IsPressedをTriggerにEnterActionsを設定することで、ボタンクリック時にアニメーションが動作します。

アニメーションではないですが、IsMouseOverがTrue時にOpacity="0.9"とすることでマウスオーバー時にボタン色を少し薄くしています。

<ControlTemplate.Triggers>
    <!--  クリック時のスタイル設定  -->
    <Trigger Property="IsPressed" Value="True">
        <Trigger.EnterActions>
            <BeginStoryboard Storyboard="{StaticResource BackColorAnimation}" />
            <BeginStoryboard Storyboard="{StaticResource ForeColorAnimation}" />
        </Trigger.EnterActions>
    </Trigger>
    <!--  マウスホバー時のスタイル設定  -->
    <Trigger Property="IsMouseOver" Value="True">
        <Setter TargetName="buttonborder" Property="Opacity" Value="0.9" />
    </Trigger>
</ControlTemplate.Triggers>

いろんな色で作ってみる

例えば、以下のようにすれば、カラフルなボタンを作成できます。設定している色は以下のサイトを参考にしています。
Flat UI Colors 2 - 14 Color Palettes, 280 colors 🎨

<Window
    x:Class="Anime_Button1.MainWindow"
    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:local="clr-namespace:Anime_Button1"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Grid>
        <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
            <local:CustomButton1
                Width="100"
                Height="40"
                Content="BUTTON" />
            <local:CustomButton1
                Width="100"
                Height="40"
                Background="#6c5ce7"
                Content="BUTTON" />
            <local:CustomButton1
                Width="100"
                Height="40"
                Background="#fd79a8"
                Content="BUTTON" />
            <local:CustomButton1
                Width="100"
                Height="40"
                Background="#fdcb6e"
                Content="BUTTON" />
        </StackPanel>
    </Grid>
</Window>


WPF スタイル エフェクトを表示するボタン2

アニメーション部分はすべて固定色として設定してOpacityを変更することで見た目上の色を変更していますので、このようにBackgroundだけを変更することで様々な色に対応できます。

ただしこれは、背景色が白の場合に限ります。背景に色がある場合、Opacityを変更すると、背景色の影響を受けてしまいます。
簡単な解決策としては、以下のように背景用のBorderを追加すれば対応可能です。

<Grid>
    <!--  ボタンの外側  -->
   ・・・省略・・・
    <!--  ボタン背景色が白以外の場合は、以下のようにOpacity="1"のBorderを追加すればよい  -->
    <Border
        Margin="4"
        Background="#ffffff"
        CornerRadius="4"
        Opacity="1" />
    <!--  ボタンのスタイル  -->
    ・・・省略・・・
    <!--  ボタン前側  -->
    ・・・省略・・・
</Grid>

様々なボタンクリックイベントの追加方法

クリック時にアニメーションを動作させたい場合このようにTriggerでIsPressedを指定するのが簡単です。IsPressedは汎用的に「クリック時の動作」として利用できるためです。例えば、以下のようなEventTriggerでMouseDownを指定する場合、ボタンのクリック動作としては反応しない右クリック動作でもStoryboardが動作してしまいます。

 <EventTrigger RoutedEvent="MouseDown">
    <BeginStoryboard>
        <Storyboard>
         ・・・省略・・・
        </Storyboard>
    </BeginStoryboard>
    <BeginStoryboard />
</EventTrigger>

左クリック動作だけに反応させたい場合は、MouseLeftButtonDownを利用する必要があります。
ただ、これはマウスの左クリックでしか利用できません。また、タッチ操作では、touchdownなど別のEventTriggerがあります。

このように、単純にクリック動作として利用したいだけでも、いろいろと考えないといけません。しかしButtonのIsPressedでは特に深く考えずに汎用的なボタンクリック動作として利用できます。


一方でIsPressedはSpaceキーでも反応しますので、キーボード入力に対応させたくない場合は注意が必要です。逆にキーボード入力に対応する場合、IsPressedではEnterキーでは反応しません。ボタンのOnClick()イベントはEnterキーでも反応しますので、Enterキーでもアニメーションを対応させたい場合はkeydownを利用するなどの対応が必要です。

WPF シンプルなCheckBoxのスタイルを作る

はじめに

ここでは、シンプルかつFlatなUIデザインで使えそうなチェックボックスのスタイルを作成します。スタイルの変更方法については以下を参考にしています。
CheckBox のスタイルとテンプレート - WPF .NET Framework | Microsoft Docs

丸いスタイル1

イメージ

作成するスタイルは以下の通りです。
WPF スタイル チェックボックス1

ソースコード

ソースコードは以下の通りです。

<Window
    x:Class="Checkbox1.MainWindow"
    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:local="clr-namespace:Checkbox1"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Window.Resources>
        <!--  チェックボックスのサイズや見た目  -->
        <Thickness x:Key="CheckBorderThickness">0.0</Thickness>
        <Thickness x:Key="UnCheckBorderThickness">2.0</Thickness>
        <CornerRadius x:Key="CheckBoxCornerRadius">20.0</CornerRadius>

        <!--  チェックボックスの色  -->
        <SolidColorBrush x:Key="CheckedBorderColor">#0984e3</SolidColorBrush>
        <SolidColorBrush x:Key="CheckedBackColor">#0984e3</SolidColorBrush>
        <SolidColorBrush x:Key="UnCheckedBorderColor">#0984e3</SolidColorBrush>
        <SolidColorBrush x:Key="UnCheckedBackColor">#ffffff</SolidColorBrush>
        <SolidColorBrush x:Key="CheckMarkColor">#ffffff</SolidColorBrush>
        <SolidColorBrush x:Key="IndeterminateMarkColor">#d63031</SolidColorBrush>

        <!--  チェックボックスのスタイル  -->
        <Style x:Key="MyCheckBox" TargetType="{x:Type CheckBox}">
            <Setter Property="SnapsToDevicePixels" Value="true" />
            <Setter Property="OverridesDefaultStyle" Value="true" />
            <Setter Property="FocusVisualStyle" Value="{DynamicResource CheckBoxFocusVisual}" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type CheckBox}">
                        <BulletDecorator>
                            <BulletDecorator.Bullet>
                                <Grid Width="20.0" Height="20.0">
                                    <!--  チェックボックス  -->
                                    <Border
                                        x:Name="CheckBoxBorder"
                                        Background="{StaticResource UnCheckedBackColor}"
                                        BorderBrush="{StaticResource UnCheckedBorderColor}"
                                        BorderThickness="{StaticResource UnCheckBorderThickness}"
                                        CornerRadius="{StaticResource CheckBoxCornerRadius}" />
                                    <!--  チェックマーク  -->
                                    <Path
                                        x:Name="CheckMark"
                                        Margin="4"
                                        Data="M0 4 L3.5 7 8 2"
                                        SnapsToDevicePixels="False"
                                        Stretch="Uniform"
                                        Stroke="{StaticResource CheckMarkColor}"
                                        StrokeThickness="2.5"
                                        Visibility="Collapsed" />
                                    <!--  不定時のチェックマーク  -->
                                    <Path
                                        x:Name="IndeterminateMark"
                                        Margin="4"
                                        Data="M 0 7 L 7 0"
                                        SnapsToDevicePixels="False"
                                        Stretch="Uniform"
                                        Stroke="{StaticResource IndeterminateMarkColor}"
                                        StrokeThickness="2.5"
                                        Visibility="Collapsed" />
                                </Grid>
                            </BulletDecorator.Bullet>
                            <ContentPresenter
                                Margin="4,0,0,0"
                                HorizontalAlignment="Left"
                                VerticalAlignment="Center" />
                        </BulletDecorator>
                        <ControlTemplate.Triggers>
                            <!--  チェック時のスタイル  -->
                            <Trigger Property="IsChecked" Value="True">
                                <Setter TargetName="CheckMark" Property="Visibility" Value="Visible" />
                                <Setter TargetName="CheckBoxBorder" Property="Background" Value="{StaticResource CheckedBackColor}" />
                                <Setter TargetName="CheckBoxBorder" Property="BorderThickness" Value="{StaticResource CheckBorderThickness}" />
                            </Trigger>
                            <!--  不定時のスタイル  -->
                            <Trigger Property="IsChecked" Value="{x:Null}">
                                <Setter TargetName="IndeterminateMark" Property="Visibility" Value="Visible" />
                            </Trigger>
                            <!--  未チェック状態でマウスオーバー時のスタイル  -->
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="IsMouseOver" Value="True" />
                                    <Condition Property="IsChecked" Value="False" />
                                </MultiTrigger.Conditions>
                                <Setter TargetName="CheckMark" Property="Visibility" Value="Visible" />
                                <Setter TargetName="CheckMark" Property="Stroke" Value="{StaticResource CheckedBackColor}" />
                                <Setter TargetName="CheckMark" Property="Opacity" Value="0.2" />
                            </MultiTrigger>
                            <!--  チェック状態でマウスオーバー時のスタイル  -->
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="IsMouseOver" Value="True" />
                                    <Condition Property="IsChecked" Value="True" />
                                </MultiTrigger.Conditions>
                                <Setter TargetName="CheckBoxBorder" Property="Opacity" Value="0.8" />
                            </MultiTrigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Grid HorizontalAlignment="Center" VerticalAlignment="Center">
        <StackPanel>
            <!--  スタイルの適用例  -->
            <CheckBox
                Content="未チェック状態"
                IsChecked="False"
                Style="{StaticResource MyCheckBox}" />
            <Label Height="10" />
            <CheckBox
                Content="チェック状態"
                IsChecked="True"
                Style="{StaticResource MyCheckBox}" />
            <Label Height="10" />
            <CheckBox
                Content="不定状態"
                IsChecked="{x:Null}"
                Style="{StaticResource MyCheckBox}" />
        </StackPanel>
    </Grid>
</Window>

解説

サイズや色の定義

まず主なパラメータをリソースとして定義します。後から異なるスタイルに変更した例も紹介しますが、こうすることでスムーズにスタイルを変更することができます(目的のスタイルが決まった場合は直接入力しても良いと思います)。


色選びについては以下のサイトが参考になるのでおすすめです。
Flat UI Colors 2 - 14 Color Palettes, 280 colors 🎨

<Window.Resources>
    <!--  チェックボックスのサイズや見た目  -->
    <Thickness x:Key="CheckBorderThickness">0.0</Thickness>
    <Thickness x:Key="UnCheckBorderThickness">2.0</Thickness>
    <CornerRadius x:Key="CheckBoxCornerRadius">20.0</CornerRadius>

    <!--  チェックボックスの色  -->
    <SolidColorBrush x:Key="CheckedBorderColor">#0984e3</SolidColorBrush>
    <SolidColorBrush x:Key="CheckedBackColor">#0984e3</SolidColorBrush>
    <SolidColorBrush x:Key="UnCheckedBorderColor">#0984e3</SolidColorBrush>
    <SolidColorBrush x:Key="UnCheckedBackColor">#ffffff</SolidColorBrush>
    <SolidColorBrush x:Key="CheckMarkColor">#ffffff</SolidColorBrush>
    <SolidColorBrush x:Key="IndeterminateMarkColor">#d63031</SolidColorBrush>
チェックボックスの背景、チェックマークの定義

Grid内に、チェックボックスの背景用のBorder、チェックマーク用のPathを作成します。
ここではチェック状態と不定状態の2つのPathを定義します。このPathの使い方は結構複雑なので、ここでは詳しく説明しません。機会があれば、別記事で取り上げようと思います。

ポイントとしては、Stretch="Uniform"とすることで親であるGridのWight、Heightが変化した場合にチェックマークのサイズも変更されるようにしています。そのためサイズ調整したい場合はGridのWight、Heightだけ調整すればよいです。

<Grid Width="20.0" Height="20.0">
    <!--  チェックボックス  -->
    <Border
        x:Name="CheckBoxBorder"
        Background="{StaticResource UnCheckedBackColor}"
        BorderBrush="{StaticResource UnCheckedBorderColor}"
        BorderThickness="{StaticResource UnCheckBorderThickness}"
        CornerRadius="{StaticResource CheckBoxCornerRadius}" />
    <!--  チェックマーク  -->
    <Path
        x:Name="CheckMark"
        Margin="4"
        Data="M0 4 L3.5 7 8 2"
        SnapsToDevicePixels="False"
        Stretch="Uniform"
        Stroke="{StaticResource CheckMarkColor}"
        StrokeThickness="2.5"
        Visibility="Collapsed" />
    <!--  不定時のチェックマーク  -->
    <Path
        x:Name="IndeterminateMark"
        Margin="4"
        Data="M 0 7 L 7 0"
        SnapsToDevicePixels="False"
        Stretch="Uniform"
        Stroke="{StaticResource IndeterminateMarkColor}"
        StrokeThickness="2.5"
        Visibility="Collapsed" />
</Grid>
チェックマークを表示させる

Triggerを利用して、IsCheckedの値に応じて表示するチェックマークの変更やBorderのBackground、BorderThicknessを変更します。
BorderThicknessについてはチェック時に0以上にして外枠と内側両方に色を設定した場合、色が綺麗に表示されないことがあったためこのようにしています。


今回はチェック状態のTriggerだけではなく、「チェックかつ、マウスオーバー」「未チェックかつ、マウスオーバー」の複合型のTriggerも設定します。
この場合は、MultiTriggerを利用することで実現可能です。

<ControlTemplate.Triggers>
    <!--  チェック時のスタイル  -->
    <Trigger Property="IsChecked" Value="True">
        <Setter TargetName="CheckMark" Property="Visibility" Value="Visible" />
        <Setter TargetName="CheckBoxBorder" Property="Background" Value="{StaticResource CheckedBackColor}" />
        <Setter TargetName="CheckBoxBorder" Property="BorderThickness" Value="{StaticResource CheckBorderThickness}" />
    </Trigger>
    <!--  不定時のスタイル  -->
    <Trigger Property="IsChecked" Value="{x:Null}">
        <Setter TargetName="IndeterminateMark" Property="Visibility" Value="Visible" />
    </Trigger>
    <!--  未チェック状態でマウスオーバー時のスタイル  -->
    <MultiTrigger>
        <MultiTrigger.Conditions>
            <Condition Property="IsMouseOver" Value="True" />
            <Condition Property="IsChecked" Value="False" />
        </MultiTrigger.Conditions>
        <Setter TargetName="CheckMark" Property="Visibility" Value="Visible" />
        <Setter TargetName="CheckMark" Property="Stroke" Value="{StaticResource CheckedBackColor}" />
        <Setter TargetName="CheckMark" Property="Opacity" Value="0.2" />
    </MultiTrigger>
    <!--  チェック状態でマウスオーバー時のスタイル  -->
    <MultiTrigger>
        <MultiTrigger.Conditions>
            <Condition Property="IsMouseOver" Value="True" />
            <Condition Property="IsChecked" Value="True" />
        </MultiTrigger.Conditions>
        <Setter TargetName="CheckBoxBorder" Property="Opacity" Value="0.8" />
    </MultiTrigger>
</ControlTemplate.Triggers>
スタイルを適用する

それぞれの状態を確認できるように3つのチェックボックスを追加して、それぞれにスタイルを適用させています。間のLabelはスペース調整のためのもので深い意味はありません。

<StackPanel>
    <!--  スタイルの適用例  -->
    <CheckBox
        Content="未チェック状態"
        IsChecked="False"
        Style="{StaticResource MyCheckBox}" />
    <Label Height="10" />
    <CheckBox
        Content="チェック状態"
        IsChecked="True"
        Style="{StaticResource MyCheckBox}" />
    <Label Height="10" />
    <CheckBox
        Content="不定状態"
        IsChecked="{x:Null}"
        Style="{StaticResource MyCheckBox}" />
</StackPanel>

丸いスタイル2

イメージ

作成するスタイルは以下の通りです。上の例とは色のパターンを変更しています。
WPF スタイル チェックボックス2

ソースコード

上の例から、以下の部分のみ変更することで対応可能です。ここでは上の例と異なりチェック状態のCheckBorderThicknessを2.0に設定してます。

    <Window.Resources>
        <!--  チェックボックスのサイズや見た目関連  -->
        <Thickness x:Key="CheckBorderThickness">2.0</Thickness>
        <Thickness x:Key="UnCheckBorderThickness">2.0</Thickness>
        <CornerRadius x:Key="CheckBoxCornerRadius">20.0</CornerRadius>

        <!--  チェックボックスの色  -->
        <SolidColorBrush x:Key="CheckedBorderColor">#00b894</SolidColorBrush>
        <SolidColorBrush x:Key="CheckedBackColor">#ffffff</SolidColorBrush>
        <SolidColorBrush x:Key="UnCheckedBorderColor">#00b894</SolidColorBrush>
        <SolidColorBrush x:Key="UnCheckedBackColor">#ffffff</SolidColorBrush>
        <SolidColorBrush x:Key="CheckMarkColor">#00b894</SolidColorBrush>
        <SolidColorBrush x:Key="IndeterminateMarkColor">#d63031</SolidColorBrush>

なお、チェックマークの色、チェックボックスの色の変更に伴い、マウスオーバー時のスタイルも少し変更しています。

<ControlTemplate.Triggers>
	・・・省略・・・
    <!--  未チェック状態でマウスオーバー時のスタイル  -->
    <MultiTrigger>
        <MultiTrigger.Conditions>
            <Condition Property="IsMouseOver" Value="True" />
            <Condition Property="IsChecked" Value="False" />
        </MultiTrigger.Conditions>
        <Setter TargetName="CheckMark" Property="Visibility" Value="Visible" />
        <Setter TargetName="CheckMark" Property="Stroke" Value="{StaticResource CheckMarkColor}" /> ←ここ
        <Setter TargetName="CheckMark" Property="Opacity" Value="0.2" />
    </MultiTrigger>
    <!--  チェック状態でマウスオーバー時のスタイル  -->
    <MultiTrigger>
        <MultiTrigger.Conditions>
            <Condition Property="IsMouseOver" Value="True" />
            <Condition Property="IsChecked" Value="True" />
        </MultiTrigger.Conditions>
        <Setter TargetName="CheckMark" Property="Opacity" Value="0.6" /> ←ここ
    </MultiTrigger>
</ControlTemplate.Triggers>

角丸スタイル1

イメージ

作成するスタイルは以下の通りです。角丸な外観にしていますが他は最初の例と変わりません。CheckBoxCornerRadiusを調整するだけで対応可能です。
WPF スタイル チェックボックス3 角丸

ソースコード

上の例と同様に以下の部分のみ変更することで対応可能です。

<Window.Resources>
    <!--  チェックボックスのサイズや見た目関連  -->
    <Thickness x:Key="CheckBorderThickness">0.0</Thickness>
    <Thickness x:Key="UnCheckBorderThickness">2.0</Thickness>
    <CornerRadius x:Key="CheckBoxCornerRadius">4.0</CornerRadius>

    <!--  チェックボックスの色  -->
    <SolidColorBrush x:Key="CheckedBorderColor">#0984e3</SolidColorBrush>
    <SolidColorBrush x:Key="CheckedBackColor">#0984e3</SolidColorBrush>
    <SolidColorBrush x:Key="UnCheckedBorderColor">#0984e3</SolidColorBrush>
    <SolidColorBrush x:Key="UnCheckedBackColor">#ffffff</SolidColorBrush>
    <SolidColorBrush x:Key="CheckMarkColor">White</SolidColorBrush>
    <SolidColorBrush x:Key="IndeterminateMarkColor">#d63031</SolidColorBrush>

角丸スタイル2

イメージ

作成するスタイルは以下の通りです。角丸な外観にしつつ2つ目の例と同じような色のパターンにしています。
WPF スタイル チェックボックス4 角丸

ソースコード

上の例と同様に以下の部分のみ変更することで対応可能です。

<Window.Resources>
    <!--  チェックボックスのサイズや見た目関連  -->
    <Thickness x:Key="CheckBorderThickness">2.0</Thickness>
    <Thickness x:Key="UnCheckBorderThickness">2.0</Thickness>
    <CornerRadius x:Key="CheckBoxCornerRadius">4.0</CornerRadius>

    <!--  チェックボックスの色  -->
    <SolidColorBrush x:Key="CheckedBorderColor">#00b894</SolidColorBrush>
    <SolidColorBrush x:Key="CheckedBackColor">#ffffff</SolidColorBrush>
    <SolidColorBrush x:Key="UnCheckedBorderColor">#00b894</SolidColorBrush>
    <SolidColorBrush x:Key="UnCheckedBackColor">#ffffff</SolidColorBrush>
    <SolidColorBrush x:Key="CheckMarkColor">#00b894</SolidColorBrush>
    <SolidColorBrush x:Key="IndeterminateMarkColor">#d63031</SolidColorBrush>

WPF シンプルなLoadingアニメーションを作る

はじめに

ここではWPFのアニメーション機能を利用して、シンプルなLoadingアニメーションを作ってみようと思います。

具体的には、以下のようなアニメーションを作成します。

WPF Loading アニメーション1

ソースコード

早速ですが、ソースコードは以下の通りです。

<Window
    x:Class="LoadingAnimation1.MainWindow"
    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:local="clr-namespace:LoadingAnimation1"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:sys="clr-namespace:System;assembly=mscorlib"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Window.Resources>
        <!--  アニメーションサイズ  -->
        <sys:Double x:Key="LoadEllipseMinSize">15.0</sys:Double>
        <sys:Double x:Key="LoadEllipseMaxSize">45.0</sys:Double>
        <!--  アニメーション時間  -->
        <KeyTime x:Key="LoadKeyTime1">00:00:00</KeyTime>
        <KeyTime x:Key="LoadKeyTime2">00:00:0.20</KeyTime>
        <KeyTime x:Key="LoadKeyTime3">00:00:0.40</KeyTime>
        <KeyTime x:Key="LoadKeyTime4">00:00:0.60</KeyTime>
        <KeyTime x:Key="LoadKeyTime5">00:00:0.80</KeyTime>
        <KeyTime x:Key="LoadKeyTimeEnd">00:00:02</KeyTime>
        <!--  アニメーション作成  -->
        <Storyboard x:Key="LoadingAnimation">
            <!--  左から1番目のEllipseアニメーション  -->
            <DoubleAnimationUsingKeyFrames
                RepeatBehavior="Forever"
                Storyboard.TargetName="LoadEllipse1"
                Storyboard.TargetProperty="Width">
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime1}" Value="{StaticResource LoadEllipseMinSize}" />
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime2}" Value="{StaticResource LoadEllipseMaxSize}" />
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime3}" Value="{StaticResource LoadEllipseMinSize}" />
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTimeEnd}" Value="{StaticResource LoadEllipseMinSize}" />
            </DoubleAnimationUsingKeyFrames>
            <!--  左から2番目のEllipseアニメーション  -->
            <DoubleAnimationUsingKeyFrames
                RepeatBehavior="Forever"
                Storyboard.TargetName="LoadEllipse2"
                Storyboard.TargetProperty="Width">
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime2}" Value="{StaticResource LoadEllipseMinSize}" />
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime3}" Value="{StaticResource LoadEllipseMaxSize}" />
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime4}" Value="{StaticResource LoadEllipseMinSize}" />
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTimeEnd}" Value="{StaticResource LoadEllipseMinSize}" />
            </DoubleAnimationUsingKeyFrames>
            <!--  左から3番目のEllipseアニメーション  -->
            <DoubleAnimationUsingKeyFrames
                RepeatBehavior="Forever"
                Storyboard.TargetName="LoadEllipse3"
                Storyboard.TargetProperty="Width">
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime3}" Value="{StaticResource LoadEllipseMinSize}" />
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime4}" Value="{StaticResource LoadEllipseMaxSize}" />
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime5}" Value="{StaticResource LoadEllipseMinSize}" />
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTimeEnd}" Value="{StaticResource LoadEllipseMinSize}" />
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>
    </Window.Resources>

    <Canvas>
        <!--  アニメーション対象  -->
        <Grid
            Canvas.Left="50"
            Canvas.Top="30"
            Height="50">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="50" />
                <ColumnDefinition Width="50" />
                <ColumnDefinition Width="50" />
            </Grid.ColumnDefinitions>
            <Ellipse
                x:Name="LoadEllipse1"
                Grid.Column="0"
                Width="{StaticResource LoadEllipseMinSize}"
                Height="{Binding ElementName=LoadEllipse1, Path=Width}"
                Fill="Gray" />
            <Ellipse
                x:Name="LoadEllipse2"
                Grid.Column="1"
                Width="{StaticResource LoadEllipseMinSize}"
                Height="{Binding ElementName=LoadEllipse2, Path=Width}"
                Fill="Gray" />
            <Ellipse
                x:Name="LoadEllipse3"
                Grid.Column="2"
                Width="{StaticResource LoadEllipseMinSize}"
                Height="{Binding ElementName=LoadEllipse3, Path=Width}"
                Fill="Gray" />
        </Grid>

        <!--  アニメーションの有効、無効の切り替え  -->
        <Button
            Canvas.Left="50"
            Canvas.Top="120"
            Width="50"
            Click="Start_Click"
            Content="Start" />
        <Button
            Canvas.Left="150"
            Canvas.Top="120"
            Width="50"
            Click="Stop_Click"
            Content="Stop" />
    </Canvas>
using System.Windows;
using System.Windows.Media.Animation;

namespace LoadingAnimation1
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Start_Click(object sender, RoutedEventArgs e)
        {
            var sb = this.Resources["LoadingAnimation"] as Storyboard;
            sb.Begin();
        }

        private void Stop_Click(object sender, RoutedEventArgs e)
        {
            var sb = this.Resources["LoadingAnimation"] as Storyboard;
            sb?.Stop();
        }
    }
}

このソースコードの実行結果は以下の通りです。
WPF Loading アニメーション2

解説

それぞれの処理内容について解説します。

サイズや実行時間の定義

ここではまずアニメーションで変化させるプロパティの値を定義しています。
必須ではありませんが、このように定義しておくと後から変更が楽になります。LoadEllipseMinSize、LoadEllipseMaxSizeを変更することで円を任意のサイズに変更できます。
なお、StringやDoubleなどのシステムの値をリソース値として定義する場合は、xmlns:sys="clr-namespace:System;assembly=mscorlib"を追加する必要があります。

<Window
    xmlns:sys="clr-namespace:System;assembly=mscorlib"
・・・省略・・・

    <Window.Resources>
        <!--  アニメーションサイズ  -->
        <sys:Double x:Key="LoadEllipseMinSize">15.0</sys:Double>
        <sys:Double x:Key="LoadEllipseMaxSize">45.0</sys:Double>
        <!--  アニメーション時間  -->
        <KeyTime x:Key="LoadKeyTime1">00:00:00</KeyTime>
        <KeyTime x:Key="LoadKeyTime2">00:00:0.20</KeyTime>
        <KeyTime x:Key="LoadKeyTime3">00:00:0.40</KeyTime>
        <KeyTime x:Key="LoadKeyTime4">00:00:0.60</KeyTime>
        <KeyTime x:Key="LoadKeyTime5">00:00:0.80</KeyTime>
        <KeyTime x:Key="LoadKeyTimeEnd">00:00:02</KeyTime>

アニメーションの作成

以下の部分で実際のアニメーションの動作を作成しています。WPFでアニメーションを作成する場合は、Storyboardを利用します。そしてStoryboard内に具体的な動作を定義します。
アニメーションの作成方法はいくつかありますが、今回のように値を柔軟に遷移させたい場合はKeyFrameを使うのが良いです。

KeyFrameにもいくつかの種類が存在します。それらの違いは、変更するプロパティの型の違いです。ここではEllipseのWightとHeightを変更したいのでDoubleAnimationUsingKeyFramesを利用します。

他のKeyFrameやアニメーションの実現方法を知りたい場合は、以下に詳しい説明が記載されています。
アニメーションの概要 - WPF .NET Framework | Microsoft Docs

    <!--  アニメーション作成  -->
    <Storyboard x:Key="LoadingAnimation">
        <!--  左から1番目のEllipseアニメーション  -->
        <DoubleAnimationUsingKeyFrames
            RepeatBehavior="Forever"
            Storyboard.TargetName="LoadEllipse1"
            Storyboard.TargetProperty="Width">
            <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime1}" Value="{StaticResource LoadEllipseMinSize}" />
            <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime2}" Value="{StaticResource LoadEllipseMaxSize}" />
            <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime3}" Value="{StaticResource LoadEllipseMinSize}" />
            <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTimeEnd}" Value="{StaticResource LoadEllipseMinSize}" />
        </DoubleAnimationUsingKeyFrames>
        <!--  左から2番目のEllipseアニメーション  -->
・・・省略・・・
        <!--  左から3番目のEllipseアニメーション  -->
・・・省略・・・
    </Storyboard>
</Window.Resources>



DoubleAnimationUsingKeyFramesで設定しているパラメータの説明は以下の通りです。(これ以外にもパラメータは存在します)

パラメータ 説明
RepeatBehavior アニメーションを繰り返す回数。Foreverにすると永遠に繰り返す。
Storyboard.TargetName 対象の名称。x:Nameで定義しておく。
Storyboard.TargetProperty 対象のプロパティ。プロパティの階層が深い場合は、正しい階層で記載する必要がある。



DoubleAnimationUsingKeyFrames内に定義するKeyFrameについても、いくつか種類が存在します。ここでは、値を滑らかに変更したいので、SplineDoubleKeyFrameを利用しています。
それぞれで設定している値は、上で定義したリソース値です。

<SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime1}" Value="{StaticResource LoadEllipseMinSize}" />
<SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime2}" Value="{StaticResource LoadEllipseMaxSize}" />
<SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime3}" Value="{StaticResource LoadEllipseMinSize}" />
<SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTimeEnd}" Value="{StaticResource LoadEllipseMinSize}" />


これに実際の値に当てはめてみると以下のようになります。

<SplineDoubleKeyFrame KeyTime="00:00:00" Value="15.0" />
<SplineDoubleKeyFrame KeyTime="00:00:0.20" Value="45.0" />
<SplineDoubleKeyFrame KeyTime="00:00:0.40" Value="15.0" />
<SplineDoubleKeyFrame KeyTime="00:00:02" Value="15.0" />

これは、以下のような動作になります。
1. 0秒の時点でのWightを15.0とする。
2. 0秒から0.20秒にかけて、Wightを15.0から45.0に変更する。
3. 0.20秒から0.40秒にかけて、Wightを45.0から15.0に変更する。
4. 0.40秒から2秒にかけて、Wightは15.0から変更しない。

一見、3か4が必要ないように思えるかもしれません。しかし3がないと、15.0から45.0への変化が0.20秒間隔で行われるのに対し、45.0から15.0への変化が0.20秒から2秒の1.80秒間隔で行われてしまうため、拡大と縮小の間隔が異なってしまいます。
また、4は3つのEllipseの終了時間を合わせるために利用しています。

<SplineDoubleKeyFrame KeyTime="00:00:00" Value="15.0" />
<SplineDoubleKeyFrame KeyTime="00:00:0.20" Value="45.0" />
<SplineDoubleKeyFrame KeyTime="00:00:0.40" Value="15.0" />
<SplineDoubleKeyFrame KeyTime="00:00:02" Value="15.0" />

ここでは、左から1番目のEllipseについてのみ説明していますが、他についても、実行時間をずらしているだけで、処理の流れは同じです。

アニメーション対象の作成

アニメーション対象の作成方法は、特に特別な処理は必要ありません。以下のCanvasや、Gridについても、アニメーションの動作確認のために定義したまでで、深い理由があるわけではありません。
ただし今回のようなコントロールのサイズを変更する場合は、以下のGridのように固定サイズのエリアを作成してからアニメーション対象を定義しないと、アニメーションによるサイズ変更に伴いコントロールの位置まで変化してしまう可能性があります。

<Canvas>
        <!--  アニメーション対象  -->
        <Grid
            Canvas.Left="50"
            Canvas.Top="30"
            Height="50">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="50" />
                <ColumnDefinition Width="50" />
                <ColumnDefinition Width="50" />
            </Grid.ColumnDefinitions>
            <Ellipse
                x:Name="LoadEllipse1"
                Grid.Column="0"
                Width="{StaticResource LoadEllipseMinSize}"
                Height="{Binding ElementName=LoadEllipse1, Path=Width}"
                Fill="Gray" />
・・・省略・・・
        </Grid>

縦横比の固定

今回のアニメーションではEllipseのサイズを変更していますが、縦横比を固定しつつサイズを変更するにはWight、Heightの両方についてStoryboardを作成する必要があります。しかしまったく同じDoubleAnimationUsingKeyFramesをWight、Heightの2つ分作成するのは面倒なので、以下のようにHeightに自分のWightをBindingすることで、縦横比を固定させています。

<Ellipse
    x:Name="LoadEllipse1"
    Grid.Column="0"
    Width="{StaticResource LoadEllipseMinSize}"
    Height="{Binding ElementName=LoadEllipse1, Path=Width}"
    Fill="Gray" />

ストーリーボードの実行、停止

ここでは、C#上から、ボタンクリックイベントに同期してストーリーボードの有効、無効を切り替えています。以下のようにリソースからストーリーボードのオブジェクトを取得した後に、storyboard.Begin();することで開始、storyboard.Stop();することで停止します。

private void Start_Click(object sender, RoutedEventArgs e)
{
    var storyboard = this.Resources["LoadingAnimation"] as Storyboard;
    storyboard.Begin();
}

private void Stop_Click(object sender, RoutedEventArgs e)
{
    var storyboard = this.Resources["LoadingAnimation"] as Storyboard;
    storyboard.Stop();
}

Python Word文書の見出しをテキストファイルに変換

はじめに

Wordで作成した文書をテキストファイルに変換するスクリプトを紹介します。python-docxを利用するので、インストールされていない場合は以下のコマンドでインストールしてください。

pip install python-docx

バージョン情報

python: 3.7.9
python-docx: 0.8.10

ソースコード

ソースコードと使用例、引数と補足説明は以下の通りです。

注意点
python-docxでは見出し番号がアウトラインとして自動設定されている場合、その番号を取得することができません。そのため、単純に変換すると見出しのレベルがわからなくなってしまいます。

見出しのレベルと番号を取得するには、そのための処理を作成する必要があります。ここでは以下の2つのパターンのソースコードを紹介します。

なお、見出し番号が自動設定ではない場合(番号がすべて直入力されている場合など)は、このような処理は不要です。

見出しレベルをスペースに変換する

関数
import docx, re

# Word文書のタイトル、見出しをテキストファイルに変換
def doc2txt_only_header(docfile, txtfile, title=True):

    full_text = []
    # wordファイルの読み込み
    doc = docx.Document(docfile)
    # 各段落を取得
    for para in doc.paragraphs:

        # paragraphオブジェクトの.style.nameからスタイルを判定        
        if title & para.style.name.startswith('Title'):
            full_text.append(para.text)
                
        if para.style.name.startswith('Heading'):
            full_text.append(convert_index_to_space(
                para.style.name) + para.text)

    # 改行文字でリストを連結して文字列に変換
    full_text = '\n'.join(full_text)
    print(full_text)

    # テキストファイルとして保存
    file_for_save = open(txtfile, 'w', encoding="utf-8")
    file_for_save.write(full_text)
    file_for_save.close()

# 見出しレベルをスペースに変換する
def convert_index_to_space(style_name):
    index_regex = re.compile('Heading (\d+)')
    result = index_regex.match(style_name)
    return ' ' * int(result.group(1))

使用例
doc2txt_only_header('sample.docx', 'sample.txt')
引数
  • docxfile:Wordファイル名(入力)
  • txtfile:テキストファイル名(出力)
  • title:表題を出力するかどうか(True/False)
出力例

サンプルのWord文書と実際の出力例は以下の通りです。少しややこしいですが、見出し番号はアウトラインで設定しており、「見出し ○○」の○○の番号は確認用としてすべて手動で入力した文字列になります。
f:id:akuruhinode:20210516174124j:plain

出力されるテキストファイルの内容は以下の通りです。

表題
 見出し1        
  見出し1.1      
   見出し1.1.1   
    見出し1.1.1.1
    見出し1.1.1.2
  見出し1.2      
 見出し2
  見出し2.1      
   見出し2.1.1   
   見出し2.1.2   

見出しレベルを通し番号に変換する

関数
import docx, re

# Word文書のタイトル、見出しをテキストファイルに変換
def doc2txt_only_header(docfile, txtfile, title=True):

    full_text = []
    # wordファイルの読み込み
    doc = docx.Document(docfile)
    # 各段落を取得
    for para in doc.paragraphs:

        # paragraphオブジェクトの.style.nameからスタイルを判定        
        if title & para.style.name.startswith('Title'):
            full_text.append(para.text)
                
        if para.style.name.startswith('Heading'):
            full_text.append(convert_index_to_number(
                para.style.name) + para.text)

    # 改行文字でリストを連結して文字列に変換
    full_text = '\n'.join(full_text)
    print(full_text)

    # テキストファイルとして保存
    file_for_save = open(txtfile, 'w', encoding="utf-8")
    file_for_save.write(full_text)
    file_for_save.close()


header_index = {}

# 見出しレベルから通し番号を作成する
def convert_index_to_number(style_name, sep='.'):
    index_regex = re.compile('Heading (\d+)')
    result = index_regex.match(style_name)
    index = int(result.group(1))

    # 各見出しレベルの番号を更新
    if index in header_index.keys():
        header_index[index] += 1

        # 上位の見出しレベルが変わったら下位の値をリセットする
        for i in range(index + 1, len(header_index) + 1):
            header_index[i] = 0
    else:
        header_index[index] = 1

    # 見出しの通し番号を作成
    full_index = ''
    for i in range(1, index + 1):
        full_index += str(header_index[i]) + sep

    # 最後のsep文字をスペースに変換
    return full_index[:-1] + ' '
出力例

見出しの番号が正しく反映されていることがわかると思います。

表題
1 見出し1
1.1 見出し1.1
1.1.1 見出し1.1.1
1.1.1.1 見出し1.1.1.1
1.1.1.2 見出し1.1.1.2
1.2 見出し1.2
2 見出し2
2.1 見出し2.1
2.1.1 見出し2.1.1
2.1.2 見出し2.1.2

使用方法、引数は見出しレベルをスペースに変換すると同じなので省略します。

補足

関数内の主要な処理はコメントの通りです。
paragraph.style.nameを確認することで、表題、見出し、本文などの種別を判別できます。
例えば、以下のようになっています。

paragraph.style.name 説明
Title 表題
Heading X 見出し(Xに見出しレベルの整数が入ります)
Normal 本文

詳しくは以下の公式HPを参考にしてください。
python-docx.readthedocs.io

Python Word文書をテキストファイルに変換

はじめに

Wordで作成した文書をテキストファイルに変換するスクリプトを紹介します。python-docxを利用するので、インストールされていない場合は以下のコマンドでインストールしてください。

pip install python-docx

バージョン情報

python: 3.7.9
python-docx: 0.8.10

ソースコード

ソースコードと使用例、引数と補足説明は以下の通りです。

メインの処理

import docx

# Word文書をテキストファイルに変換
def docx2txt(docxfile, txtfile):

    full_text = []

    # wordファイルの読み込み
    doc = docx.Document(docxfile)
    # 各段落を取得
    for para in doc.paragraphs:
        # paragraphオブジェクトのtext属性からテキスト文字列を取得
        full_text.append(para.text)

    # 改行文字でリストを連結して文字列に変換
    doc_text = '\n'.join(full_text)
    # print(doc_text)

    # テキストファイルとして保存
    file_for_save = open(txtfile, 'w', encoding="utf-8")
    file_for_save.write(doc_text)
    file_for_save.close()

使用例

docx2txt('sample.docx', 'sample.txt')

引数

  • docxfile:Wordファイル名(入力)
  • txtfile:テキストファイル名(出力)

補足

関数内の主要な処理はコメントの通りです。
python-docxを利用してWordファイルを読み込んだ場合、ファイルの内容はparagraphというオブジェクトのリストで構成されています。

paragraphは一つの段落を示しており、いくつかの属性を持ちます。例えばtextはテキスト文字列です。他には文書のスタイルに関する属性などがあります。
詳しくは以下の公式HPを参考にしてください。

python-docx.readthedocs.io

その他のWord関連スクリプト

ここでは文書内のすべての文字列を抽出しましたが、見出し部分のみ抽出するスクリプトはこちらの記事で紹介しています。
Python Word文書の見出しをテキストファイルに変換 - akuruhinode's blog

Python 最近更新されたファイルのリストをCSVで保存

はじめに

PCを利用しているといつの間にかファイルが増えていき、ストレージ容量が不足してしまうことがあります。自分で作成、保存したファイルであれば手動で管理できるのですが、OSやアプリが自動生成するファイルについては管理が難しいという課題を抱えていました。

特にサイズが大きいファイルであればまだ分かりやすいのですが、小さなファイルが大量に作成される場合は調査が難しい状況でした。
そこで、ここでは特定の日付以降に更新されたファイルのリストを出力するスクリプトを作成しました。

バージョン情報

python: 3.7.9
pandas: 1.2.3

ソースコード

ソースコードと使用例は以下の通りです。

メインの処理

import os
import glob
import datetime
import pandas as pd

# 指定した日付以降更新されたファイルのリストを保存する
def save_updated_files(save_name, files, date, get_size=False):
    updated_files = []

    for path in files:
        if not os.path.isfile(path):
            continue

        try:
            # 更新日を取得
            updated_time = os.path.getmtime(path)
            updated_time = datetime.datetime.fromtimestamp(updated_time)
            # 更新日の比較
            if updated_time >= date:
                info = [path, updated_time]
                # ファイルサイズを取得する(オプション)
                if get_size:
                    info.append(os.path.getsize(path))

                updated_files.append(info)

        # ファイルにアクセスできない場合は除外する
        except OSError: 
            continue

    print('files:', len(updated_files))

    columns = ['file', 'date']
    if get_size:
        columns.append('size(bytes)')

    # CSVで保存
    df = pd.DataFrame(updated_files, columns=columns)
    df.to_csv(save_name, index=False, encoding='shift_jis')

使用例

# ファイルのリストを取得
file_list = glob.glob(r'C:\example\**\*.*', recursive=True)

# 2021年1月1日以降更新されたファイルのリストを保存する
save_updated_files('updated_files.csv',
                  file_list, datetime.datetime(2021, 1, 1, 0, 0, 0), get_size=True)

引数

  • save_name:出力するCSVファイルの名前
  • files:ファイルリスト
  • date:基準とする日付(datetime型)
  • get_size:サイズを出力するかどうか

補足

検索対象のファイルリスト(フォルダ)については自由度があったほうが良いと思い、ここでは関数の外で処理しています。例えば特定のフォルダ内のファイルのみ対象にする、複数のフォルダのファイルを対象にする、特定の拡張子のファイルのみを対象にするなどです。

関数内の主要な処理はコメントの通りです。オプションとして引数でget_size=Trueとすることでファイルのサイズも取得できるようにしています。

注意点としては、ファイルにアクセスできない場合にはOSErrorが発生する可能性があるということです。ここではtry - except文で除外しています。

Python ファイル名の一括変換

はじめに

ここでは、任意のフォルダ内のファイル名を一括変換する方法を説明します。

バージョン情報

python: 3.7.9
pandas: 1.2.3

ソースコード

この処理にはいくつかの手順が必要なので、一連の処理を行う関数を作成しました。ソースコードと引数の説明は以下の通りです。

メインの処理

import os, glob, re, shutil
import pandas as pd

# フォルダ内のファイル名を一括変換する
def rename_files(path, rename_regex, replace_str='', check=True, rename=True):
 
    # 現在のファイル名のリストを取得
    old_file_list = glob.glob(path)
    # 変換後のファイル名のリストを作成
    new_file_list = list(map(lambda x: (re.sub(rename_regex, replace_str,
                    os.path.splitext(x)[0]) + os.path.splitext(x)[1]),
                    old_file_list))

    # 変換後のファイル名を確認する
    if check:
        df = pd.DataFrame(zip(old_file_list, new_file_list),
                          columns=['old_filename', 'new_filename'])
        print(df)
        df.to_csv('rename_list.csv', index=False, encoding='shift_jis')

    # 変換を行う
    if rename:
        # 念のため確認の入力を設ける
        if input('proceed?(y/n)') == 'y':
            for old_name, new_name in zip(old_file_list, new_file_list):
                shutil.move(old_name, new_name)

使用例

# ファイル名を一括変換
rename_files(path=r'C:\example\*.txt', rename_regex='\-1$')

引数

  • path:フォルダパス
  • rename_regex:変換する文字列(正規表現
  • replace_str:変換後の文字列(デフォルトでは空文字)
  • check:変換後の文字列をCSVに出力する
  • rename:変換を実施する

補足

この例では、C:\example以下のすべてのテキストファイルについて、ファイル名の末尾から'-1'という文字列を削除しています。rename_regexreplace_strを任意の値に変更することで、様々な変換を実現可能です。

変換前 変換後
aaa-1.txt aaa.txt
bbb-1.txt bbb.txt

pathにて以下のようにすれば、すべての拡張子のファイルに対して変換を行うことができます。

# ファイル名を一括変換
rename_files(path=r'C:\example\*.*', rename_regex='\-1$')

なお、上記関数はファイルの拡張子は変換対象外としています。

注意点

フォルダのパスや正規表現を間違えると、想定外の変換が行われてしまう可能性があります。
それを防ぐためにここでは2つの引数を追加しています。

checkでTrueを指定すると、変換結果例をCSVに出力します(コマンドライン上にも表示しますが、ファイル数やファイル名が長いと確認し難いことが想定されますのでCSVでの確認を推奨します)。
renameではTrueを指定した場合のみ、変換を実施します。そのため、まずはcheck=Truerename=Falseで変換結果を事前に確認したうえで、実際の変換を行うと安心です。


さらに、rename=Trueとした場合でも、変換前に以下の確認文字列が表示されます。ここで、'y'以外を入力すると、変換を実施しません。

proceed?(y/n)