Create a thumbnail of an inactive C# WPF Window

2020-06-04 01:06发布

I've looked through many topics here, and googled for the information, but I haven't found anything relating to my question.

What I want to do is have it so when a user starts the application, the main window (not an MDI) opens with four imageboxes, each showing an image of the form that would open when they click on it. Once the selected form is open, and changes are made, if they click to minimize/close the form, it will (seemingly) minimize into the imagebox showing a real-time image of what the form looks like in a thumbnail view.

My question is, how do I make a form into an image so I can use the image as a thumbnail in an imagebox?

Also... Can someone point me in the direction of some resources that will help me figure out how to animate the "minimizing" into the imagebox?

I'm not asking anyone to do my work for me, because I'd like to learn it myself, but I'm kinda stuck.

Lastly, I'm not sure what's involved in this, so I don't know what tags to put for this post. I'll add tags as I figure it out so others can find this information.

EDIT: Sorry, it is in WPF. Wasn't sure it would be any different. I'm still not particularly experienced in WPF.

标签: c# wpf
3条回答
放荡不羁爱自由
2楼-- · 2020-06-04 01:40

You can use the VisualBrush, here is a quick example of a button with a background set to a downscaled version of a stackpanel.

 <DockPanel>
        <StackPanel x:Name="myRect" >
            <TextBox Text="MyTexasdfasdfasdfasdfasdft" Height="50" />
            <CheckBox IsChecked="True"  />
            <Rectangle Fill="Red" Width="100" Height="100" />
        </StackPanel>


        <Button>
            <Button.Background>
                <VisualBrush TileMode="None"  Viewport="0,0,1,1" Visual="{Binding ElementName=myRect}" >
                    <VisualBrush.Transform>
                        <ScaleTransform ScaleX="0.3" ScaleY="0.3" />
                    </VisualBrush.Transform>
                </VisualBrush>
            </Button.Background>
        </Button>
    </DockPanel>

Edit: though this solution works to copy stuff that is on the screen, when the stuff on screen is hidden or removed, so will the VisualBrush. In order to persist the image, it is necessary to render the control to a bitmap. This can be done with the RenderTargetBitMap

// CenterControl is the target to render, ShowControl is the control to render the CenterControl onto.
var rtb = new RenderTargetBitmap((int)CenterControl.ActualWidth, (int)CenterControl.ActualHeight, 96, 96,
                                             PixelFormats.Pbgra32);
            rtb.Render(CenterControl);
            var bgBrush = new ImageBrush(rtb) {Transform = new ScaleTransform(0.1, 0.1)};
            ShowControl.Background = bgBrush;
查看更多
家丑人穷心不美
3楼-- · 2020-06-04 01:42

I'm going to assume you want actual separate windows that can be dragged and dropped independently around your screen among other applications' windows. (If this assumption is incorrect and a MDI-like interface is better for you, take a look at Rob's answer.)

I would implement an Expander subclass that accepts a Window and:

  1. When IsExpanded=false, it presents the window content itself using a ContentPresenter, but
  2. When IsExpanded=true, it lets the window present its own content but uses a VisualBrush with a Rectangle to display that content

It might be named "WindowExpander" and would have its Content property set to the actual Window object to be shown when the Expander is expanded. For example it could be used in one of these ways, depending on how your Windows are defined:

<UniformGrid Rows="2" Columns="2">
  <local:WindowExpander Window="{StaticResource Form1Window}" />
  <local:WindowExpander Window="{StaticResource Form2Window}" />
  <local:WindowExpander Window="{StaticResource Form3Window}" />
  <local:WindowExpander Window="{StaticResource Form4Window}" />
</UniformGrid>

<UniformGrid Rows="2" Columns="2">
  <local:WindowExpander><Window Width="800" Height="600"><local:Form1 /></Window></local:WindowExpander>
  <local:WindowExpander><Window Width="800" Height="600"><local:Form2 /></Window></local:WindowExpander>
  <local:WindowExpander><Window Width="800" Height="600"><local:Form3 /></Window></local:WindowExpander>
  <local:WindowExpander><Window Width="800" Height="600"><local:Form4 /></Window></local:WindowExpander>
</UniformGrid>

<ItemsControl ItemsSource="{Binding Forms}">
  <ItemsControl.ItemsPanel>
    <ItemsPanelTemplate><UniformGrid Rows="2" Columns="2"/></ItemsPanelTemplate>
  </ItemsControl.ItemsPanel>
</ItemsControl>

The implementation of WindowExpander would be a ToggleButton containing a ViewBox that displayed the the thumbnail, like this:

<Style TargetType="local:WindowExpander">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="local:WindowExpander">
        <ToggleButton IsChecked="{TemplateBinding IsExpanded}">
          <Viewbox IsHitTestVisible="False">
            <ContentPresenter Content="{Binding Header} />
          </Viewbox>
        </ToggleButton>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

I'm thinking you would probably want to implement WindowExpander something like this:

[ContentProperty("Window")]
public class WindowExpander : Expander
{
  Storyboard _storyboard;

  public static WindowExpander()
  {
    DefaultStyleKeyProperty.OverrideMetadata(typeof(WindowExpander), new FrameworkPropertyMetadata(typeof(WindowExpander)));
    IsExpandedProperty.OverrideMetadata(typeof(WindowExpander), new FrameworkPropertyMetadata
    {
      PropertyChangedCallback = (obj, e) =>
        {
          var expander = (WindowExpander)obj;
          if(expander.Window!=null)
          {
            expander.SwapContent(expander.Window);
            expander.AnimateWindow();
          }
        }
    });
  }

  public Window Window { get { return (Window)GetValue(WindowProperty); } set { SetValue(WindowProperty, value); } }
  public static readonly DependencyProperty WindowProperty = DependencyProperty.Register("Window", typeof(Window), typeof(WindowExpander), new UIPropertyMetadata
  {
    PropertyChangedCallback = (obj, e) =>
    {
      var expander = (WindowExpander)obj;
      var oldWindow = (Window)e.OldValue;
      var newWindow = (Window)e.NewValue;
      if(oldWindow!=null)
      {
        if(!expander.IsExpanded) expander.SwapContent(oldWindow);
        oldWindow.StateChanged -= expander.OnStateChanged;
      }
      expander.ConstructLiveThumbnail();
      if(newWindow!=null)
      {
        if(!expander.IsExpanded) expander.SwapContent(newWindow);
        newWindow.StateChanged -= expander.OnStateChanged;
      }
    }
  });

  private void ConstructLiveThumbnail()
  {
    if(Window==null)
      Header = null;
    else
    {
      var rectangle = new Rectangle { Fill = new VisualBrush { Visual = (Visual)Window.Content } };
      rectangle.SetBinding(Rectangle.WidthProperty, new Binding("Width") { Source = Window });
      rectangle.SetBinding(Rectangle.HeightProperty, new Binding("Height") { Source = Window });
      Header = rectangle;
    }
  }

  private void SwapContent(Window window)
  {
    var a = Header; var b = window.Content;
    Header = null;  window.Content = null;
    Header = b;     window.Content = a;
  }

  private void AnimateWindow()
  {
    if(_storyboard!=null)
      _storyboard.Stop(Window);

    var myUpperLeft = PointToScreen(new Point(0, 0));
    var myLowerRight = PointToScreen(new Point(ActualWidth, ActualHeight));
    var myRect = new Rect(myUpperLeft, myLowerRight);
    var winRect = new Rect(Window.Left, Window.Top, Window.Width, Window.Height);

    var fromRect = IsExpanded ? myRect : winRect;  // Rect where the window will animate from
    var toRect = IsExpanded ? winRect : myRect;    // Rect where the window will animate to

    _storyboard = new Storyboard { FillBehavior = FillBehavior.Stop };
    // ... code to build storyboard here ...
    // ... should animate "Top", "Left", "Width" and "Height" of window from 'fromRect' to 'toRect' using desired timeframe
    // ... should also animate Visibility=Visibility.Visible at time=0

    _storyboard.Begin(Window);
    Window.Visibility = IsExpanded ? Visibility.Visible : Visibility.Hidden;
  }

  private void OnStateChanged(object sender, EventArgs e)
  {
    if(IsExpanded && Window.WindowState == WindowState.Minimized)
    {
      Window.WindowState = WindowState.Normal;
      IsExpanded = false;
    }
  }
}

The above code omits the steps to construct the animation. It also has not been tested - it was just written quickly off the top of my head. I hope it works for you.

How it works: IsExpanded controls the Window's visibility, except that when IsExpanded changes the storyboard temporarily forces the window to stay visible long enough for the animation run. At any given moment either the Window or the ContentPresenter in the template contains the window's content. You might say that whenever the expander is not expanded (no window), the Content is "stolen" from the window for use within the WindowExpander. This is done by the SwapContent method. It puts the Rectangle painted with the VisualBrush into the Window and the Window's actual content into the Header, which is the thumbnail shown on the ToggleButton.

This technique works around the fact that VisualBrush doesn't work on an invisible Visual because the "Content" visual is actually always visible - it is always a child of either the Window or of the ContentPresenter inside the ViewBox.

Because a VisualBrush is used, the thumbnail always gives a live preview.

One caveat: Don't set a DataContext or create resources at the Window level. If you do, when your content is "stolen" it will not have the right DataContext or resources so it won't look right. My recommendation would be to use a UserControl instead of a Window for each form, and have your main form wrap it in a Window as illustrated here:

<UniformGrid Rows="2" Columns="2">
  <local:WindowExpander><Window Width="800" Height="600"><local:Form1 /></Window></local:WindowExpander>
  <local:WindowExpander><Window Width="800" Height="600"><local:Form2 /></Window></local:WindowExpander>
  <local:WindowExpander><Window Width="800" Height="600"><local:Form3 /></Window></local:WindowExpander>
  <local:WindowExpander><Window Width="800" Height="600"><local:Form4 /></Window></local:WindowExpander>
</UniformGrid>
查看更多
beautiful°
4楼-- · 2020-06-04 02:01

If you're beginning with WPF, then what you're planning to do will likely require that you either learn Blend in order to define the conditions and animations, or dive deep into the animation system in order to understand it and hand-code the XAML.

At a high level, I imagine you could approach this by defining each of your four "forms" as UserControls or ContentPresenters, perhaps with a Border around them.

Then, when the "form" is in an inactive state, use the LayoutTransform or RenderTransform property along with other positioning properties to position and shrink it. Once your brain is accustomed to Blend it's actually pretty easy to define this using the "States" and "Triggers".

To add a behavior to grow the minimized form, handle the "PreviewMouseDown" event and in the handler, test for the state of the form.

I found the "Learn Blend in 5 Days" videos useful for this, but I'll confess to sharing your confusion; there is no unified place that I've found which teaches XAML and WPF in a systematic way, without simply enrolling in a third-party training class or calling in a mentor-consultant. It doesn't help that at this time, the fifth day of the training is "Coming Soon", or that the entire thing is keyed to Silverlight rather than WPF.

But, it's a start; The "Learn Blend" videos are found here:

http://www.microsoft.com/expression/resources/blendtraining/

You'll also see a link to something called ".toolbox", which I haven't yet tried.

查看更多
登录 后发表回答