使用具有相同布局的动态 TabItem 实现 TabControl

Implement a TabControl with dynamic TabItem with the same layout

我将尝试通过我的程序界面描述我想要实现的目标:

它是一个连接到服务器的客户端,用于在该服务器 运行 所在的特定环境中检索有关 运行 应用程序的一些信息。到目前为止,它只适用于一台服务器,因此如果它已连接,则需要断开连接并重新连接到另一台服务器以获取有关另一台服务器 运行 的另一台电脑的信息。我想允许我的客户端创建更多实例以连接到其他服务器以同时获取有关更多 pc 的信息。

我的设计理念是通过一个TabControl扩展我在MainWindow中设计的界面。通过“+”或任何按钮,我想创建客户端界面的另一个实例,但我只需要维护以红色突出显示的元素,而以绿色突出显示的元素必须对所有实例都是通用的。

我现在有一个 window,我把所有的控件都放在其中以显示进程和其他一些东西,同样,TabControl 中的所有选项卡必须具有相同的布局但内容不同,但它们必须共享相同的 LogWindow(绿框)。

图中显示的TabControl是从另一个图像简单复制粘贴的,我还没有实现它。

我看过一些关于动态选项卡控件项的教程,特别是 this one

问题是我没有正确理解如何将我的 MainWindow 布局移植为其他选项卡的标准模板。

这是我开发的代码:

MainWindow.xaml

<Window x:Class="Project_Client.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:Project_Client"
    xmlns:helper="clr-namespace:Helper"
    mc:Ignorable="d"
    ResizeMode="CanResizeWithGrip"
    MinHeight="480" MinWidth="640"
    Title="MainWindow" Height="480" Width="640" Closing="Window_Closing" 
    >
<Window.Resources>
    <BitmapImage x:Key="RevealImage" UriSource="./reveal_icon.png"></BitmapImage>
    <BitmapImage x:Key="HideImage" UriSource="./hide_icon.png"></BitmapImage>
</Window.Resources>

<Grid>
    
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"></ColumnDefinition>
            <ColumnDefinition Width="auto"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <TextBlock FontWeight="Bold" VerticalAlignment="Top" Width="86" Height="16" Margin="13,76,0,0" HorizontalAlignment="Left">Process List:</TextBlock>

        <Grid Grid.Column="0" Name="mLeftGrid" Margin="0,0,0,183.4" HorizontalAlignment="Stretch">

            <Grid.Resources>
                <DataTemplate x:Key="StrProperty">
                    <TextBlock Text="{Binding Path=Str}"/>
                </DataTemplate>
                <DataTemplate x:Key="ImgProperty">
                    <Image Source="{Binding Path=Image}"  />
                </DataTemplate>
            </Grid.Resources>
            <Grid.RowDefinitions>
                <RowDefinition Height="228*"/>
                <RowDefinition Height="343*"/>
            </Grid.RowDefinitions>
            <Border Grid.RowSpan="2"
        BorderBrush="Cyan"
        BorderThickness="3"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch"/>
            <ProgressBar x:Name="progressBarProcess" Margin="13,66,-3,0" Height="6" VerticalAlignment="Top" HorizontalAlignment="Left" Width="316"/>
            <TextBlock x:Name="progressText" HorizontalAlignment="Stretch" TextWrapping="Wrap" Text="TextBlock" Width="auto" Margin="12,45,-12,0" Height="19" VerticalAlignment="Top"/>

            <Grid Name="mLeftInnerGrid" Margin="9.8,102,10,0" VerticalAlignment="Top" Grid.RowSpan="2" HorizontalAlignment="Stretch" SizeChanged="mLeftInnerGrid_SizeChanged">

                <ListView x:Name="mCurrentApps" HorizontalAlignment="Left" Height="auto" VerticalAlignment="Top" ItemsSource="{Binding Items, Mode=OneWay}" VirtualizingPanel.IsVirtualizing="true" 
     VirtualizingPanel.VirtualizationMode="Recycling" Margin="0,0,-155.2,0" SizeChanged="mCurrentApps_SizeChanged">
                    <ListView.ItemContainerStyle>
                        <Style TargetType="{x:Type ListViewItem}">
                            <Setter Property="Focusable" Value="False"/>
                            <Style.Triggers>
                                <Trigger Property="IsSelected" Value="True" >
                                    <Setter Property="FontWeight" Value="Bold" />
                                    <Setter Property="Foreground" Value="OrangeRed" />
                                </Trigger>
                            </Style.Triggers>
                            <Style.Resources>
                                <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent"/>
                            </Style.Resources>
                        </Style>
                    </ListView.ItemContainerStyle>
                    <ListView.View>
                        <GridView>
                            <GridViewColumn>
                                <GridViewColumn.Header>
                                    <GridViewColumnHeader Tag="App" Click="GridViewColumnHeader_Click_" Width="auto">App</GridViewColumnHeader>
                                </GridViewColumn.Header>
                                <GridViewColumn.CellTemplate>
                                    <DataTemplate>
                                        <StackPanel Orientation="Horizontal" MinWidth="64">
                                            <Image Width="16" Height="16" Source="{Binding Image}" Margin="4,0,16,0" />
                                            <TextBlock Text="{Binding App}" />
                                        </StackPanel>
                                    </DataTemplate>
                                </GridViewColumn.CellTemplate>
                            </GridViewColumn>
                            <GridViewColumn DisplayMemberBinding="{Binding Process}" >
                                <GridViewColumn.Header>
                                    <GridViewColumnHeader Tag="Process" Click="GridViewColumnHeader_Click_" Width="auto">Process</GridViewColumnHeader>
                                </GridViewColumn.Header>
                            </GridViewColumn>
                            <GridViewColumn FrameworkElement.FlowDirection="RightToLeft">
                                <GridViewColumn.CellTemplate>
                                    <DataTemplate>
                                        <TextBlock Text="{Binding ExecutionTimer}" HorizontalAlignment="Right"></TextBlock>
                                    </DataTemplate>
                                </GridViewColumn.CellTemplate>
                                <GridViewColumn.Header >
                                    <GridViewColumnHeader Tag="Timer" Click="GridViewColumnHeader_Click_" HorizontalContentAlignment="Right" >Active Time</GridViewColumnHeader>
                                </GridViewColumn.Header>
                            </GridViewColumn>
                        </GridView>
                    </ListView.View>

                </ListView>

            </Grid>

        </Grid>

        <Grid Grid.Column="1" Name="mRigthGrid" Margin="0,0,-0.4,182.4" HorizontalAlignment="Right">
            <Grid.RenderTransform>
                <TranslateTransform x:Name="mRightGridAnim"></TranslateTransform>
            </Grid.RenderTransform>
            <Border
        BorderBrush="Coral"
        BorderThickness="3"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch"/>
            <TextBlock FontWeight="Bold" Margin="0,76,248,0" HorizontalAlignment="Right" VerticalAlignment="Top" Width="auto" Height="16">Status:</TextBlock>
            <TextBlock x:Name="mAppStatus" Visibility="Visible" Width="auto" Margin="0,76,45,0" Height="16" VerticalAlignment="Top" HorizontalAlignment="Right"/>
            <TextBox x:Name="mKeyPressed" HorizontalAlignment="Right" Margin="0,98,28,149" TextWrapping="Wrap" Text="..." Width="247" Loaded="keyPressed_Loaded" FontSize="20" IsReadOnly="True"/>
            <Button x:Name="mRecordingButton" Content="Start recording keys" Margin="0,0,77,98" Click="button_Click" HorizontalAlignment="Right" Width="139" Height="19" VerticalAlignment="Bottom"/>
            <TextBlock x:Name="PcName" Margin="8,45,229,0" TextWrapping="Wrap" Text="PC-??????" HorizontalAlignment="Right" Width="57" Height="15" VerticalAlignment="Top" />
            <TextBlock x:Name="numberOfProcesses" HorizontalAlignment="Right" Margin="0,45,132,0" TextWrapping="Wrap" Text="Processes: ???" Height="16" VerticalAlignment="Top"/>

                    </Grid>
        
    </Grid>

    <DockPanel x:Name="mRevealButton" PreviewMouseLeftButtonUp="mRevealButton_PreviewMouseLeftButtonUp" Margin="0,97,-0.4,183.4" HorizontalAlignment="Right" Width="10">
        <DockPanel.Style>
            <Style TargetType="{x:Type DockPanel}">
                <Style.Triggers>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter Property="Background" Value="LightGray"/>
                        <Setter Property="ToolTip" Value="Click to show/hide keys input box"/>
                    </Trigger>

                </Style.Triggers>
            </Style>
        </DockPanel.Style>
        <Image x:Name="mRevealImage" Width="16" Source="reveal_icon.png" HorizontalAlignment="Center" VerticalAlignment="Center" />
    </DockPanel>
    <Rectangle x:Name="Background" Visibility="Hidden">
        <Rectangle.Fill>
            <SolidColorBrush Color="{DynamicResource {x:Static SystemColors.InactiveCaptionColorKey}}"/>
        </Rectangle.Fill>
    </Rectangle>
    <ListBox x:Name="logWindow" ItemsSource="{Binding LogEntry, Mode=OneWay}" Grid.IsSharedSizeScope="True" VirtualizingPanel.IsVirtualizing="true" 
     VirtualizingPanel.VirtualizationMode="Recycling" Margin="10,0,8.6,41.4" Height="134" VerticalAlignment="Bottom" >
        <ListBox.ItemContainerStyle>
            <Style TargetType="{x:Type ListBoxItem}" >
                <Setter Property="FontStyle" Value="Italic"></Setter>
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Path=IsError, UpdateSourceTrigger=PropertyChanged}" Value="true">
                        <Setter Property="Foreground" Value="Red"></Setter>

                    </DataTrigger>
                    <DataTrigger Binding="{Binding Path=IsWarning, UpdateSourceTrigger=PropertyChanged}" Value="true">
                    </DataTrigger>

                </Style.Triggers>
                <Style.Resources>
                    <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent"/>

                </Style.Resources>
            </Style>
        </ListBox.ItemContainerStyle>
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="auto" SharedSizeGroup="Icon" />
                        <ColumnDefinition Width="auto" MinWidth="64" SharedSizeGroup="Log" />
                    </Grid.ColumnDefinitions>
                    <Image Grid.Column="0"  Source="{Binding Path = Img}" Width="16" Height="16"/>
                    <TextBlock Grid.Column="1" Text="{Binding Path = log}" Margin="4,0,0,0" TextTrimming="CharacterEllipsis" HorizontalAlignment="Stretch" />
                </Grid>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
    <Image x:Name="noConnectionImage" Source="no_connection.png" Width="256" Height="256" HorizontalAlignment="Center" VerticalAlignment="Center" Opacity="0.6" Visibility="Hidden" Margin="194,19,183.6,175.4" />
    <TextBlock x:Name="mBlockServerName" VerticalAlignment="Bottom" HorizontalAlignment="Left" Margin="10,0,0,10.4" TextWrapping="Wrap" Text="Server:" Grid.Row="1" />
    <TextBox x:Name="mBoxServerName" VerticalAlignment="Bottom" HorizontalAlignment="Left" Margin="69,0,0,10.4" TextWrapping="Wrap" Text="" Width="123" Grid.Row="1"  />
    <TextBlock x:Name="mBlockServerPort" VerticalAlignment="Bottom" HorizontalAlignment="Left" Margin="210,0,0,10.4" TextWrapping="Wrap" Text="Port:" Grid.Row="1"/>
    <TextBox x:Name="mBoxServerPort" VerticalAlignment="Bottom" HorizontalAlignment="Left" Margin="251,0,0,10.4" TextWrapping="Wrap" Text="" Width="47" Grid.Row="1" />
    <Button x:Name="mButtonConnect" VerticalAlignment="Bottom" HorizontalAlignment="Right" Content="Connect"  Margin="0,0,104.6,10.4" Width="76" Click="mButtonConnect_Click" Grid.Row="1"/>
    <Button x:Name="mButtonDisconnect" VerticalAlignment="Bottom" HorizontalAlignment="Right" Content="Disconnect"  Margin="0,0,9.6,10.4"  Width="75" Grid.Row="1" Click="mButtonDisconnect_Click"/>
</Grid>

MainWindow.cs(仅对填充视图有用的方法)

public ObservableCollection<ProcessData> Items { get; set; }
public ObservableCollection<LogItem> LogEntry { get; set; }
public static MainWindow myInstance;
private LogItem log;
private Client client;
ProcessData indexFocused;
List<ProcessData> lop = new List<ProcessData>();
public String serverIp;
public long port;


public MainWindow()
{
    InitializeComponent();  
    myInstance = this;
    InitializeUI();
    attachLogSource();
    this.DataContext = this;
}

public void InitializeUI()
{
    mButtonConnect.IsEnabled = true;
    mAppStatus.Text = "Disconnected.";
    noConnectionImage.Visibility = Visibility.Visible;
    Background.Visibility = Visibility.Visible;
    //mCurrentApps.Visibility = Visibility.Hidden;
    //Items = new ObservableCollection<ProcessData>();
    noConnectionImage.Visibility = Visibility.Visible;
    Background.Visibility = Visibility.Visible;
    noConnectionImage.Source = new BitmapImage(new Uri(thisPath +  "no_connection.png"));
    progressBarProcess.Visibility = Visibility.Hidden;
    progressText.Visibility = Visibility.Hidden;
    //PcName.Visibility = Visibility.Hidden;
   // numberOfProcesses.Visibility = Visibility.Hidden;
    mBoxServerName.Text = "127.0.0.1";
    mBoxServerPort.Text = "11000";
    mButtonDisconnect.IsEnabled = false;
    //mCurrentApps.Visibility = Visibility.Hidden;
    mRecordingButton.IsEnabled = true;
    mRevealButton.Visibility = Visibility.Visible;
    mRevealImage.Source = new BitmapImage(new Uri(thisPath + "hide_icon.png"));
    GridViewColumnResizeBehaviour b = new GridViewColumnResizeBehaviour();
    b.Attach(mCurrentApps);
}

private void mButtonConnect_Click(object sender, RoutedEventArgs e)
{
    client = new Client();
    attachListSource();
    
    if (!(mBoxServerName.Text.Trim() == String.Empty && mBoxServerPort.Text.Trim() == String.Empty))
    {
        Items.Clear();
        log = new LogItem();
        log.log = "Connecting...";
        LogMessage(log);
        //mCurrentApps.Visibility = Visibility.Hidden;
        mRecordingButton.IsEnabled = false;
        client.wr_monitor();
    }
    else
    {
        log.log = "Must insert a valid address and port.";
        LogMessage(log);
    }
        //MessageBox.Show("Must insert a valid server address and port", "Error", MessageBoxButton.OK, MessageBoxImage.Exclamation);
}

private void attachListSource()
{
    Items = new ObservableCollection<ProcessData>();
    mCurrentApps.ItemsSource = Items;
    Items.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(takeFocus);
}

private void attachLogSource()
{
    if(LogEntry == null)
    {
        LogEntry = new ObservableCollection<LogItem>();
        logWindow.ItemsSource = LogEntry;
        LogEntry.CollectionChanged += (s, e) =>
        {
            if (e.Action ==
                System.Collections.Specialized.NotifyCollectionChangedAction.Add)
            {
                logWindow.ScrollIntoView(logWindow.Items[logWindow.Items.Count - 1]);
            }
        };
    }
}

private void updateList()
{
    var l = lop.OrderBy(o => o.App);
    this.Dispatcher.Invoke(new Action(() =>
    {
        mCurrentApps.Visibility = Visibility.Visible;
        mCurrentApps.ItemsSource = l;
        progressText.Text = "Complete! (" + lop.Count + ")/(" + lop.Count + ")";
        numberOfProcesses.Text = "Processes: " + Items.Count.ToString();
        log = new LogItem();
        log.log = "Received all the processes (" + lop.Count + ")";
        LogMessage(log);
        mCurrentApps.Items.Refresh();
    }));
}

private void mButtonDisconnect_Click(object sender, RoutedEventArgs e)
{
    Client.EndConnect();
    mButtonConnect.IsEnabled = true;
}

private void LogMessage(LogItem l)
{
    l.Img = SystemIcons.Information.ToImageSource() as BitmapSource;
    Application.Current.Dispatcher.Invoke(() =>
    {
        LogEntry.Add(l);
    });
}

private void GridViewColumnHeader_Click_(object sender, RoutedEventArgs e)
{
    ListViewColumnOptions.GridViewColumnHeader_Click_(sender, e);
}
private void mRevealButton_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    DockPanelOptions.PerformClickAction(sender, e);
}

private void mCurrentApps_SizeChanged(object sender, SizeChangedEventArgs e)
{
    ListViewColumnOptions.ListView_SizeChanged(sender, e);
}

private void mLeftInnerGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
        mCurrentApps.Width = mLeftInnerGrid.ActualWidth;   
}

特别是我只想在选项卡之间共享视图之间通用的列表框部分 (logWindow),但我想用不同的数据填充每个选项卡。

据我所知,我需要创建一个 userControl 并将其作为数据模板在将要创建的每个选项卡中实现,但我不知道如何在不参考预构建示例的情况下实现它。

您可以通过简单的数据绑定来完成(只是因为问题有点含糊,所以才有这个想法):

<Window x:Class="FileHelperTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="2*"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <TabControl ItemsSource="{Binding AllServers}" SelectedItem="{Binding SelectedServer, Mode=TwoWay}">
        <TabControl.ItemTemplate>
            <!-- this is the header template-->
            <DataTemplate>
                <Grid>
                <TextBlock Text="{Binding Name}"/>
                <Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
                    Content="{Binding Name}"
                    Command="{Binding DataContext.AddNew, RelativeSource={RelativeSource AncestorType=TabControl}, Mode=OneTime}" 
                    CommandParameter="{Binding}" >
                    <Button.Style>
                        <Style TargetType="{x:Type  Button}">
                            <Setter Property="Visibility" Value="Collapsed"/>
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding Name}" Value="+" >
                                    <Setter Property="Visibility" Value="Visible"/>
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </Button.Style>
                </Button>
                </Grid>
            </DataTemplate>
        </TabControl.ItemTemplate>
        <TabControl.ContentTemplate>
            <!-- this is the body of the TabItem template-->
            <DataTemplate>
                <TextBlock Text="{Binding SomeServerPropertyToDisplay}" />
            </DataTemplate>
        </TabControl.ContentTemplate>
    </TabControl>
    <DataGrid ItemsSource="{Binding LogMessages}" Grid.Row="1"/>
    <StackPanel Orientation="Horizontal" Grid.Row="2">
        <TextBlock Text="{Binding Path=SelectedServer.Name, StringFormat=Current server: {0}}"/>
        <Button Command="{Binding Connect, Mode=OneTime}" Content="Connect"/>
        <Button Command="{Binding Disconnect, Mode=OneTime}" Content="Disconnect"/>
    </StackPanel>
</Grid>

(以下代码使用 nuget 包 Fody.PropertyChanged 和 ICommand 的标准实现)

[ImplementPropertyChanged]
public partial class Window1 : Window
{
    MyViewModel VM;
    public Window1()
    {
        InitializeComponent();
        this.VM = new MyViewModel();
        this.DataContext = this.VM;
    }
}

[ImplementPropertyChanged]
public class MyViewModel
{
    public ObservableCollection<LogMessage> LogMessages { get; set; }
    public ObservableCollection<ServerTab> AllServers { get; set; }
    public ServerTab SelectedServer { get; set; }

    public MyViewModel()
    {
        this.AllServers = new ObservableCollection<ServerTab>();
        this.AllServers.Add(new ServerTab { Name = "+" });

        this.LogMessages = new ObservableCollection<LogMessage>();
    }

    private ICommand _AddNew;
    public ICommand AddNew { get { return _AddNew ?? (_AddNew = new DelegateCommand(a => AddNewCommand(a))); }}
    private void AddNewCommand(object item)
    {
        var senderItem = (ServerTab)item;
        if (senderItem.Name == "+")
        {
            var newItem = new ServerTab { Name = "Name " + (this.AllServers.Count) };
            this.AllServers.Remove(senderItem);
            this.AllServers.Add(newItem);
            this.AllServers.Add(senderItem);
            this.LogMessages.Add(new LogMessage { Time = DateTime.Now, Message = newItem.Name + " added" });
        }
    }

    private ICommand _Connect;
    public ICommand Connect{ get { return _Connect ?? (_Connect = new DelegateCommand(a => ConnectCommand(a))); }}
    private void ConnectCommand(object item)
    {
        this.LogMessages.Add(new LogMessage { Time = DateTime.Now, Message = this.SelectedServer.Name+" connected" });
    }

    private ICommand _Disconnect;
    public ICommand Disconnect{ get { return _Disconnect ?? (_Disconnect = new DelegateCommand(a => DisconnectCommand(a))); }}
    private void DisconnectCommand(object item)
    {
        this.LogMessages.Add(new LogMessage { Time = DateTime.Now, Message = this.SelectedServer.Name +" disconnected" });
    }
}

[ImplementPropertyChanged]
public class ServerTab
{
    public string Name { get; set; }
    public string SomeServerPropertyToDisplay { get; set; }
    public ServerTab()
    {
        SomeServerPropertyToDisplay = DateTime.Now.Ticks.ToString();
    }
}

[ImplementPropertyChanged]
public class LogMessage
{
    public DateTime Time { get; set; }
    public string Message { get; set; }
}

编辑: 我用 MVVM

的基本实现更新了代码