none
組織図のようなツリー表示について RRS feed

  • 質問

  • 表題の通り、ツリービューコントロールなどを使用して、VisualStudioのクラスダイアグラムのような組織図を作成したいと考えておりますが、ツリービューコントロールではそのような表示設定などが見当たりませんでした。

    ※以下のような構造を縦表示、及びボタン or 画像のような表示

    親ノード
    ├子ノード
    │└孫ノード
    └子ノード
     ├孫ノード
     └孫ノード

    開発環境はvisual studio community 2017で、WinForm(WPFでも検討中)になります。

    上記のようなコントロール、またはサンプルなどがあればご教授願います。

    2018年3月27日 10:10

回答

  • こんな?

    <Window x:Class="WpfApplication1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    				xmlns:app="clr-namespace:WpfApplication1"
            Title="MainWindow" Height="350" Width="525">
    	<Window.Resources>
    		<app:LastConverter x:Key="lastConv" />
    		<app:OrientationConverter x:Key="oriConv" />
    		<Orientation x:Key="orientation">Vertical</Orientation>
    
    		<ItemsPanelTemplate x:Key="orgTreePanelTemplate">
    			<VirtualizingStackPanel Tag="{DynamicResource orientation}" Orientation="{Binding RelativeSource={RelativeSource Mode=Self},Path=Tag,Converter={StaticResource oriConv}}" />
    		</ItemsPanelTemplate>
    		
    		
    		<Style x:Key="TreeViewItemFocusVisual">
    			<Setter Property="Control.Template">
    				<Setter.Value>
    					<ControlTemplate>
    						<Rectangle/>
    					</ControlTemplate>
    				</Setter.Value>
    			</Setter>
    		</Style>
    		<Style x:Key="ExpandCollapseToggleStyle" TargetType="{x:Type ToggleButton}">
    			<Setter Property="Focusable" Value="False"/>
    			<Setter Property="Height" Value="13"/>
    			<Setter Property="Template">
    				<Setter.Value>
    					<ControlTemplate TargetType="{x:Type ToggleButton}">
    						<Border Background="Transparent" Height="13">
    							<Border BorderBrush="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" BorderThickness="1" Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}" Height="9" SnapsToDevicePixels="true" Width="9">
    								<Path x:Name="ExpandPath" Data="M 0 2 L 0 3 L 2 3 L 2 5 L 3 5 L 3 3 L 5 3 L 5 2 L 3 2 L 3 0 L 2 0 L 2 2 Z" Fill="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}" Margin="1,0,1,0"/>
    							</Border>
    						</Border>
    						<ControlTemplate.Triggers>
    							<Trigger Property="IsChecked" Value="True">
    								<Setter Property="Data" TargetName="ExpandPath" Value="M 0 2 L 0 3 L 5 3 L 5 2 Z"/>
    							</Trigger>
    						</ControlTemplate.Triggers>
    					</ControlTemplate>
    				</Setter.Value>
    			</Setter>
    		</Style>
    		<Style x:Key="TreeViewItemStyle1" TargetType="{x:Type TreeViewItem}">
    			<Setter Property="ItemsPanel" Value="{StaticResource orgTreePanelTemplate}" />
    
    			<Setter Property="Template">
    				<Setter.Value>
    					<ControlTemplate TargetType="{x:Type TreeViewItem}">
    						<StackPanel x:Name="stack" Orientation="{DynamicResource ResourceKey=orientation}" >
    							<Grid Width="10" x:Name="branch">
    								<Grid.RowDefinitions>
    									<RowDefinition />
    									<RowDefinition />
    								</Grid.RowDefinitions>
    								<Border Grid.Row="0" BorderBrush="Black" BorderThickness="0,0,0,1" x:Name="b0"/>
    								<Border Grid.Row="0" BorderBrush="Black" BorderThickness="1,0,0,0" x:Name="b1"/>
    								<Border Grid.Row="1" BorderBrush="Black" BorderThickness="1,0,0,0" x:Name="b2"/>
    							</Grid>
    							
    							<StackPanel Orientation="{DynamicResource ResourceKey=orientation}" VerticalAlignment="Center" HorizontalAlignment="Center">
    								<Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Grid.Column="1" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
    									<ContentPresenter x:Name="PART_Header" ContentSource="Header" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
    								</Border>
    								
    								<ToggleButton x:Name="Expander" ClickMode="Press" IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}" Style="{StaticResource ExpandCollapseToggleStyle}"
    															VerticalAlignment="Center" HorizontalAlignment="Center"/>
    							</StackPanel>
    							<ItemsPresenter x:Name="ItemsHost" />
    						</StackPanel>
    
    						<ControlTemplate.Triggers>
    							
    							<Trigger Property="IsExpanded" Value="false">
    								<Setter Property="Visibility" TargetName="ItemsHost" Value="Collapsed"/>
    							</Trigger>
    							<Trigger Property="HasItems" Value="false">
    								<Setter Property="Visibility" TargetName="Expander" Value="Hidden"/>
    							</Trigger>
    
    							<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=PreviousData}}" Value="{x:Null}">
    								<Setter TargetName="b1" Property="BorderThickness" Value="0,0,0,0" />
    							</DataTrigger>
    							<DataTrigger Value="true">
    								<DataTrigger.Binding>
    									<MultiBinding Converter="{StaticResource lastConv}">
    										<Binding Path="Items" RelativeSource="{RelativeSource Mode=FindAncestor,AncestorType=TreeViewItem}" />
    										<Binding Path="Items.Count" RelativeSource="{RelativeSource Mode=FindAncestor,AncestorType=TreeViewItem}" />
    										<Binding Path="." />
    									</MultiBinding>
    								</DataTrigger.Binding>
    								<Setter TargetName="b2" Property="BorderThickness" Value="0,0,0,0" />
    							</DataTrigger>
    
    							<DataTrigger Binding="{Binding ElementName=stack,Path=Orientation}" Value="Vertical" >
    								<Setter TargetName="branch" Property="LayoutTransform">
    									<Setter.Value>
    										<TransformGroup>
    											<RotateTransform Angle="-90"/>
    											<ScaleTransform ScaleY="-1" />
    										</TransformGroup>
    									</Setter.Value>
    								</Setter>
    								<Setter TargetName="ItemsHost" Property="HorizontalAlignment" Value="Center" />
    							</DataTrigger>
    							<DataTrigger Binding="{Binding ElementName=stack,Path=Orientation}" Value="Horizontal" >
    								<Setter TargetName="ItemsHost" Property="VerticalAlignment" Value="Center" />
    							</DataTrigger>
    						</ControlTemplate.Triggers>
    					</ControlTemplate>
    				</Setter.Value>
    			</Setter>
    			
    		</Style>
    	</Window.Resources>
    	
    	<DockPanel>
    		<Button Content="Create" Click="Button_Click" DockPanel.Dock="Bottom"/>
    		<Button Content="Orientation" Click="Button_Click_1" DockPanel.Dock="Bottom"/>
    		<TreeView ItemsSource="{Binding}" ItemContainerStyle="{DynamicResource TreeViewItemStyle1}"
    							ScrollViewer.VerticalScrollBarVisibility="Auto"
    							ScrollViewer.HorizontalScrollBarVisibility="Auto"
    							ItemsPanel="{StaticResource orgTreePanelTemplate}">
    			<TreeView.ItemTemplate>
    				<HierarchicalDataTemplate ItemsSource="{Binding Path=Children}">
    					<Border BorderBrush="Black" BorderThickness="1" Margin="0,2">
    						<TextBlock Text="{Binding Path=Name}" />
    					</Border>
    				</HierarchicalDataTemplate>
    			</TreeView.ItemTemplate>
    		</TreeView>
    	</DockPanel>
    </Window>
    using System;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    
    namespace WpfApplication1
    {
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
                this.DataContext = new Item[] { Item.CreateTest() };
            }
    
            private void Button_Click(object sender, RoutedEventArgs e)
            {
                this.DataContext = new Item[] { Item.CreateTest() };
            }
    
            private void Button_Click_1(object sender, RoutedEventArgs e)
            {
                var o = this.FindResource("orientation");
                if (o is Orientation)
                {
                    Orientation ori = (Orientation)o;
                    this.Resources["orientation"]= (ori == Orientation.Horizontal) ? Orientation.Vertical : Orientation.Horizontal;
                }
            }
        }
    
        class Item
        {
            static Random rnd = new Random();
            public static Item CreateTest()
            {
               
                Item root = new Item();
                root.Name = "ROOT";
    
                CreateTest(root);
                return root;
            }
            private static void CreateTest(Item parent,int depth=0)
            {
                int count=rnd.Next(0, 10-depth);
                for (int i = 0; i < count; i++)
                {
                    Item item1 = new Item();
                    item1.Name = string.Empty.PadLeft(rnd.Next(1, 20), '*');
                    parent.Children.Add(item1);
    
                    CreateTest(item1,depth+1);
                }
            }
    
            public Item()
            {
                Children = new System.Collections.ObjectModel.ObservableCollection<Item>();
    
            }
            public System.Collections.ObjectModel.ObservableCollection<Item> Children { get; private set; }
    
            public string Name { get; set; }
        }
    
        class LastConverter : IMultiValueConverter
        {
    
            public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
            {
                if (values.Length < 3)
                {
                    return true;
                }
               
                var list = values[0] as System.Collections.IList;
    
                if (list==null || list.Count == 0)
                {
                    return true;
                }
                return list[list.Count - 1] == values[2];
            }
    
            public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
            {
                throw new NotSupportedException();
            }
        }
    
        class OrientationConverter : IValueConverter
        {
    
            public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
            {
                if (value is Orientation)
                {
                    return (Orientation)value == Orientation.Horizontal ? Orientation.Vertical : Orientation.Horizontal;
                }
                return value;
            }
    
            public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
            {
                throw new NotImplementedException();
            }
        }
    }



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

    2018年3月27日 17:49

すべての返信

  • Google 画像検索「VisualStudioのクラスダイアグラム」
    https://www.google.co.jp/search?q=VisualStudio%E3%81%AE%E3%82%AF%E3%83%A9%E3%82%B9%E3%83%80%E3%82%A4%E3%82%A2%E3%82%B0%E3%83%A9%E3%83%A0&source=lnms&tbm=isch&sa=X&ved=0ahUKEwigrO6_yYzaAhUJI5QKHaWZDIoQ_AUICigB&biw=1536&bih=735#imgrc=_

    文字表現の階層表示ではなく、図形表現の階層表示を作成されたいということで合っていますか?
    有償コントロールですが、Infragistics 社様の xamOrgChart コントロールを利用することで、しん11111111 様の実現したいものが作成できるのではないかと思います。

    自前で作成する場合は、WinForms よりも WPF の方が楽に実現できる気がします。
    Canvas, Rectangle, Border + 任意のコントロール, Geometry を利用した矢印コネクタ、等々を組み合わせて、重ならない位置を計算して図形配置しながら実装することになるかと思います。

    ちなみにですが、
    左側に TreeView、右側に UserControl や DataGridView などの組み合わせでの、組織図の画面だとNGですよね?
    こちらだと難易度がぐっと下がるのですが。
    2018年3月27日 14:26
  • sutefu7様ご回答ありがとうございます。

    >有償コントロールですが、Infragistics 社様の xamOrgChart コントロールを利用することで、しん11111111 様の実現したいものが作成できるのではないかと思います。

    Infragistics 社様の xamOrgChart コントロールのようなもので認識に相違ありません。無償(3rd party)、または.NET 標準コントロールでは用意されていないということでしょうか。

    >自前で作成する場合は、WinForms よりも WPF の方が楽に実現できる気がします。
    >Canvas, Rectangle, Border + 任意のコントロール, Geometry を利用した矢印コネクタ、等々を組み合わせて、重ならない位置を計算して図形配置しながら実装することになるかと思います。

    WPFの自作コントロールは少し難易度が高く感じておりますので、参考となるURLなどがあればご教授頂けると幸いです。

    >左側に TreeView、右側に UserControl や DataGridView などの組み合わせでの、組織図の画面だとNGですよね?
    こちらだと難易度がぐっと下がるのですが。

    私も上記の内容で最初は考えておりましたが、横表示しかできないのと組織図と比べるとだいぶ異なってしまうので廃案としました。


    2018年3月27日 15:55
  • こんな?

    <Window x:Class="WpfApplication1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    				xmlns:app="clr-namespace:WpfApplication1"
            Title="MainWindow" Height="350" Width="525">
    	<Window.Resources>
    		<app:LastConverter x:Key="lastConv" />
    		<app:OrientationConverter x:Key="oriConv" />
    		<Orientation x:Key="orientation">Vertical</Orientation>
    
    		<ItemsPanelTemplate x:Key="orgTreePanelTemplate">
    			<VirtualizingStackPanel Tag="{DynamicResource orientation}" Orientation="{Binding RelativeSource={RelativeSource Mode=Self},Path=Tag,Converter={StaticResource oriConv}}" />
    		</ItemsPanelTemplate>
    		
    		
    		<Style x:Key="TreeViewItemFocusVisual">
    			<Setter Property="Control.Template">
    				<Setter.Value>
    					<ControlTemplate>
    						<Rectangle/>
    					</ControlTemplate>
    				</Setter.Value>
    			</Setter>
    		</Style>
    		<Style x:Key="ExpandCollapseToggleStyle" TargetType="{x:Type ToggleButton}">
    			<Setter Property="Focusable" Value="False"/>
    			<Setter Property="Height" Value="13"/>
    			<Setter Property="Template">
    				<Setter.Value>
    					<ControlTemplate TargetType="{x:Type ToggleButton}">
    						<Border Background="Transparent" Height="13">
    							<Border BorderBrush="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" BorderThickness="1" Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}" Height="9" SnapsToDevicePixels="true" Width="9">
    								<Path x:Name="ExpandPath" Data="M 0 2 L 0 3 L 2 3 L 2 5 L 3 5 L 3 3 L 5 3 L 5 2 L 3 2 L 3 0 L 2 0 L 2 2 Z" Fill="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}" Margin="1,0,1,0"/>
    							</Border>
    						</Border>
    						<ControlTemplate.Triggers>
    							<Trigger Property="IsChecked" Value="True">
    								<Setter Property="Data" TargetName="ExpandPath" Value="M 0 2 L 0 3 L 5 3 L 5 2 Z"/>
    							</Trigger>
    						</ControlTemplate.Triggers>
    					</ControlTemplate>
    				</Setter.Value>
    			</Setter>
    		</Style>
    		<Style x:Key="TreeViewItemStyle1" TargetType="{x:Type TreeViewItem}">
    			<Setter Property="ItemsPanel" Value="{StaticResource orgTreePanelTemplate}" />
    
    			<Setter Property="Template">
    				<Setter.Value>
    					<ControlTemplate TargetType="{x:Type TreeViewItem}">
    						<StackPanel x:Name="stack" Orientation="{DynamicResource ResourceKey=orientation}" >
    							<Grid Width="10" x:Name="branch">
    								<Grid.RowDefinitions>
    									<RowDefinition />
    									<RowDefinition />
    								</Grid.RowDefinitions>
    								<Border Grid.Row="0" BorderBrush="Black" BorderThickness="0,0,0,1" x:Name="b0"/>
    								<Border Grid.Row="0" BorderBrush="Black" BorderThickness="1,0,0,0" x:Name="b1"/>
    								<Border Grid.Row="1" BorderBrush="Black" BorderThickness="1,0,0,0" x:Name="b2"/>
    							</Grid>
    							
    							<StackPanel Orientation="{DynamicResource ResourceKey=orientation}" VerticalAlignment="Center" HorizontalAlignment="Center">
    								<Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Grid.Column="1" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
    									<ContentPresenter x:Name="PART_Header" ContentSource="Header" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
    								</Border>
    								
    								<ToggleButton x:Name="Expander" ClickMode="Press" IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}" Style="{StaticResource ExpandCollapseToggleStyle}"
    															VerticalAlignment="Center" HorizontalAlignment="Center"/>
    							</StackPanel>
    							<ItemsPresenter x:Name="ItemsHost" />
    						</StackPanel>
    
    						<ControlTemplate.Triggers>
    							
    							<Trigger Property="IsExpanded" Value="false">
    								<Setter Property="Visibility" TargetName="ItemsHost" Value="Collapsed"/>
    							</Trigger>
    							<Trigger Property="HasItems" Value="false">
    								<Setter Property="Visibility" TargetName="Expander" Value="Hidden"/>
    							</Trigger>
    
    							<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=PreviousData}}" Value="{x:Null}">
    								<Setter TargetName="b1" Property="BorderThickness" Value="0,0,0,0" />
    							</DataTrigger>
    							<DataTrigger Value="true">
    								<DataTrigger.Binding>
    									<MultiBinding Converter="{StaticResource lastConv}">
    										<Binding Path="Items" RelativeSource="{RelativeSource Mode=FindAncestor,AncestorType=TreeViewItem}" />
    										<Binding Path="Items.Count" RelativeSource="{RelativeSource Mode=FindAncestor,AncestorType=TreeViewItem}" />
    										<Binding Path="." />
    									</MultiBinding>
    								</DataTrigger.Binding>
    								<Setter TargetName="b2" Property="BorderThickness" Value="0,0,0,0" />
    							</DataTrigger>
    
    							<DataTrigger Binding="{Binding ElementName=stack,Path=Orientation}" Value="Vertical" >
    								<Setter TargetName="branch" Property="LayoutTransform">
    									<Setter.Value>
    										<TransformGroup>
    											<RotateTransform Angle="-90"/>
    											<ScaleTransform ScaleY="-1" />
    										</TransformGroup>
    									</Setter.Value>
    								</Setter>
    								<Setter TargetName="ItemsHost" Property="HorizontalAlignment" Value="Center" />
    							</DataTrigger>
    							<DataTrigger Binding="{Binding ElementName=stack,Path=Orientation}" Value="Horizontal" >
    								<Setter TargetName="ItemsHost" Property="VerticalAlignment" Value="Center" />
    							</DataTrigger>
    						</ControlTemplate.Triggers>
    					</ControlTemplate>
    				</Setter.Value>
    			</Setter>
    			
    		</Style>
    	</Window.Resources>
    	
    	<DockPanel>
    		<Button Content="Create" Click="Button_Click" DockPanel.Dock="Bottom"/>
    		<Button Content="Orientation" Click="Button_Click_1" DockPanel.Dock="Bottom"/>
    		<TreeView ItemsSource="{Binding}" ItemContainerStyle="{DynamicResource TreeViewItemStyle1}"
    							ScrollViewer.VerticalScrollBarVisibility="Auto"
    							ScrollViewer.HorizontalScrollBarVisibility="Auto"
    							ItemsPanel="{StaticResource orgTreePanelTemplate}">
    			<TreeView.ItemTemplate>
    				<HierarchicalDataTemplate ItemsSource="{Binding Path=Children}">
    					<Border BorderBrush="Black" BorderThickness="1" Margin="0,2">
    						<TextBlock Text="{Binding Path=Name}" />
    					</Border>
    				</HierarchicalDataTemplate>
    			</TreeView.ItemTemplate>
    		</TreeView>
    	</DockPanel>
    </Window>
    using System;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    
    namespace WpfApplication1
    {
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
                this.DataContext = new Item[] { Item.CreateTest() };
            }
    
            private void Button_Click(object sender, RoutedEventArgs e)
            {
                this.DataContext = new Item[] { Item.CreateTest() };
            }
    
            private void Button_Click_1(object sender, RoutedEventArgs e)
            {
                var o = this.FindResource("orientation");
                if (o is Orientation)
                {
                    Orientation ori = (Orientation)o;
                    this.Resources["orientation"]= (ori == Orientation.Horizontal) ? Orientation.Vertical : Orientation.Horizontal;
                }
            }
        }
    
        class Item
        {
            static Random rnd = new Random();
            public static Item CreateTest()
            {
               
                Item root = new Item();
                root.Name = "ROOT";
    
                CreateTest(root);
                return root;
            }
            private static void CreateTest(Item parent,int depth=0)
            {
                int count=rnd.Next(0, 10-depth);
                for (int i = 0; i < count; i++)
                {
                    Item item1 = new Item();
                    item1.Name = string.Empty.PadLeft(rnd.Next(1, 20), '*');
                    parent.Children.Add(item1);
    
                    CreateTest(item1,depth+1);
                }
            }
    
            public Item()
            {
                Children = new System.Collections.ObjectModel.ObservableCollection<Item>();
    
            }
            public System.Collections.ObjectModel.ObservableCollection<Item> Children { get; private set; }
    
            public string Name { get; set; }
        }
    
        class LastConverter : IMultiValueConverter
        {
    
            public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
            {
                if (values.Length < 3)
                {
                    return true;
                }
               
                var list = values[0] as System.Collections.IList;
    
                if (list==null || list.Count == 0)
                {
                    return true;
                }
                return list[list.Count - 1] == values[2];
            }
    
            public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
            {
                throw new NotSupportedException();
            }
        }
    
        class OrientationConverter : IValueConverter
        {
    
            public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
            {
                if (value is Orientation)
                {
                    return (Orientation)value == Orientation.Horizontal ? Orientation.Vertical : Orientation.Horizontal;
                }
                return value;
            }
    
            public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
            {
                throw new NotImplementedException();
            }
        }
    }



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

    2018年3月27日 17:49
  • こんなところで探す

    https://marketplace.visualstudio.com/
    https://www.codeproject.com/

    こんな感じ

    https://marketplace.visualstudio.com/search?term=treeview&target=VS&category=All%20categories&vsVersion=&sortBy=Relevance
    https://www.codeproject.com/search.aspx?q=treeview


    Jitta@わんくま同盟

    2018年3月27日 23:36
  • gekka様 シンプルで的確なサンプルをご提供頂きありがとうございます。

    頂いたサンプルを元に改良させて頂きます。

    2018年3月28日 0:00