I cannot seem to draw a smooth curve over a bar chart (in the same chart). I do not see any StackOverflow questions pertaining (but maybe missed).
Anyway, to do this, I was thinking of using a ColumnSeries for the bars with a LineSeries for the curve. Issues seem to be:
1) Points that are in the LineSeries but not in the ColumnSeries always appear at the end in the chart (even if the X values are in between those that exist in the ColumnSeries). I was thinking these in between values could be used to make the LineSeries curve smooth.
2) The LineSeries also controls the spacing between X values. All I want is the LineSeries to be used to draw the smooth curve between the bars. As it is, having many points in the LineSeries makes the bars very thin where they should be thick. In other words, the ColumnSeries ideally would control the tick marks and bar thickness (not the LineSeries).
The xaml is
<Grid>
<chartingToolkit:Chart x:Name="MainChart" HorizontalAlignment="Left" Margin="59,35,0,0" Title="Chart Title" VerticalAlignment="Top" Height="246" Width="405">
<chartingToolkit:ColumnSeries DependentValuePath="Value" IndependentValuePath="Key" ItemsSource="{Binding BarCollection, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<chartingToolkit:LineSeries DependentValuePath="Value" IndependentValuePath="Key" ItemsSource="{Binding LineCollection, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</chartingToolkit:Chart>
</Grid>
The code behind is
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
// Add sample curve points
LineCollection.Add(new KeyValuePair<double, double>(1, 11));
LineCollection.Add(new KeyValuePair<double, double>(1.5, 18));
LineCollection.Add(new KeyValuePair<double, double>(2, 22));
LineCollection.Add(new KeyValuePair<double, double>(2.5, 35));
LineCollection.Add(new KeyValuePair<double, double>(3, 42));
// Primary bars being added
BarCollection.Add(new KeyValuePair<double, double>(1, 10));
BarCollection.Add(new KeyValuePair<double, double>(2, 20));
BarCollection.Add(new KeyValuePair<double, double>(3, 40));
}
/// <summary>
/// Core event handler for the view model.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Routes property changed events.
/// </summary>
/// <param name="propertyName"> Defines the property name upon which routing is determined. </param>
public void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
ObservableCollection<KeyValuePair<double, double>> barCollection = new ObservableCollection<KeyValuePair<double, double>>();
public ObservableCollection<KeyValuePair<double, double>> BarCollection
{
get
{
return barCollection;
}
set
{
barCollection = value;
}
}
ObservableCollection<KeyValuePair<double, double>> lineCollection = new ObservableCollection<KeyValuePair<double, double>>();
public ObservableCollection<KeyValuePair<double, double>> LineCollection
{
get
{
return lineCollection;
}
set
{
lineCollection = value;
}
}
}
Thanks,
Buck
Related
I have a need to have a XAML object always be scaled 1:1, or atleast the imagebrush content, even though it's parent is in a viewbox and the content is compressed in the X direction.
An example: Viewbox contains Label & ImageBrush. I'd like the label text to scale, but only the ImageBrush size - when zoomed out it would only display the top corner of the content.
The viewmodel for the object does not have access to the scale factor. I've been looking for a way to remove or reset the viewbox transform, but I've been unable to find one. Is there one or will I have to propagate the current scale factor from the parent to the end viewmodel? I'd rather not mix presentation logic there unless I absolutely must.
Here's the current XAML I've got so far:
<ImageBrush ImageSource="{Binding Paint, Mode=OneWay, Converter={StaticResource BitmapToImageSourceConverter}}">
<ImageBrush.RelativeTransform>
<MatrixTransform>
<MatrixTransform.Matrix>
<MultiBinding Converter="{StaticResource TimelineMatrixConverter}">
<Binding />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type MatrixTransform}}" Path="Matrix" />
</MultiBinding>
</MatrixTransform.Matrix>
</MatrixTransform>
</ImageBrush.RelativeTransform>
</ImageBrush>
Here is a basic exemple of what you can do. First extend the Viewbox class:
public class ViewboxEx : Viewbox
{
private FrameworkElement _child;
#region InvertScaleH
/// <summary>
/// InvertScaleH Read-Only Dependency Property
/// </summary>
private static readonly DependencyPropertyKey InvertScaleHPropertyKey
= DependencyProperty.RegisterReadOnly("InvertScaleH", typeof(double), typeof(ViewboxEx),
new FrameworkPropertyMetadata((double)1));
public static readonly DependencyProperty InvertScaleHProperty
= InvertScaleHPropertyKey.DependencyProperty;
/// <summary>
/// Gets the InvertScaleH property. This dependency property
/// indicates invert scale factor to compensate for horizontal scale fo the Viewbox.
/// </summary>
public double InvertScaleH
{
get { return (double)GetValue(InvertScaleHProperty); }
}
/// <summary>
/// Provides a secure method for setting the InvertScaleH property.
/// This dependency property indicates invert scale factor to compensate for horizontal scale fo the Viewbox.
/// </summary>
/// <param name="value">The new value for the property.</param>
protected void SetInvertScaleH(double value)
{
SetValue(InvertScaleHPropertyKey, value);
}
#endregion
#region InvertScaleV
/// <summary>
/// InvertScaleV Read-Only Dependency Property
/// </summary>
private static readonly DependencyPropertyKey InvertScaleVPropertyKey
= DependencyProperty.RegisterReadOnly("InvertScaleV", typeof(double), typeof(ViewboxEx),
new FrameworkPropertyMetadata((double)1));
public static readonly DependencyProperty InvertScaleVProperty
= InvertScaleVPropertyKey.DependencyProperty;
/// <summary>
/// Gets the InvertScaleV property. This dependency property
/// indicates invert scale factor to compensate for vertical scale fo the Viewbox.
/// </summary>
public double InvertScaleV
{
get { return (double)GetValue(InvertScaleVProperty); }
}
/// <summary>
/// Provides a secure method for setting the InvertScaleV property.
/// This dependency property indicates invert scale factor to compensate for vertical scale fo the Viewbox.
/// </summary>
/// <param name="value">The new value for the property.</param>
protected void SetInvertScaleV(double value)
{
SetValue(InvertScaleVPropertyKey, value);
}
#endregion
public ViewboxEx()
{
Loaded += OnLoaded;
SizeChanged += (_,__) => UpdateScale();
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
UpdateChild();
}
protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
{
base.OnVisualChildrenChanged(visualAdded, visualRemoved);
UpdateChild();
}
private void UpdateChild()
{
if (_child != null)
{
_child.SizeChanged -= OnChild_SizeChanged;
}
_child = Child as FrameworkElement;
if (_child != null)
{
_child.SizeChanged += OnChild_SizeChanged;
}
UpdateScale();
}
private void OnChild_SizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateScale();
}
private void UpdateScale()
{
if (_child == null)
return;
SetInvertScaleH(_child.ActualWidth / ActualWidth);
SetInvertScaleV(_child.ActualHeight / ActualHeight);
}
}
Basically what it does is to listen for the changes of the size of the Viewbox and its content and then calculate the invert scale factor. Now to use this scale factor:
<Window x:Class="YourNamespace.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:YourNamespace"
Title="MainWindow" Height="450" Width="800">
<Grid>
<local:ViewboxEx>
<Grid Width="100" Height="100">
<Ellipse Fill="Green"/>
<TextBlock Text="123" Margin="10,10,0,0">
<TextBlock.RenderTransform>
<ScaleTransform
ScaleX="{Binding InvertScaleH, RelativeSource={RelativeSource AncestorType={x:Type local:ViewboxEx}}}"
ScaleY="{Binding InvertScaleV, RelativeSource={RelativeSource AncestorType={x:Type local:ViewboxEx}}}"/>
</TextBlock.RenderTransform>
</TextBlock>
</Grid>
</local:ViewboxEx>
</Grid>
</Window>
In this exemple the Ellipse follows the Viewbox size change as usual but the text keeps its size.
This is a basic implementation and may not work in all cases.
Very high up the visual tree in WPF's Expander control is a border element (see screenshot). By default this has a CornerRadius of 3. Is it possible to modify this value?
I'll leave marking as answer for now but I managed to implement the solution as follows:
Using stylesnooper I obtained the style / control template used for the 'standard' Expander control.
Then after discovering it didn't quite behave as expected, figured out that the line <ToggleButton IsChecked="False" ... is wrong and should actually be <ToggleButton IsChecked="{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"...
Everything then worked as expected.
I made a behavior which modifies the first found border in the ControlTemplate. You can easily extend the behavior with new properties where u want to modify
/// <summary>
/// modifies the first found <see cref="Border"/> in the <see cref="ControlTemplate"/> of the attached <see cref="Control"/>
/// </summary>
public class ModifyBorderBehavior : Behavior<Control>
{
// ##############################################################################################################################
// Properties
// ##############################################################################################################################
#region Properties
/// <summary>
/// The new corner radius
/// </summary>
public CornerRadius CornerRadius
{
get => (CornerRadius)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
/// <summary>
/// The <see cref="CornerRadius"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(ModifyBorderBehavior));
#endregion
// ##############################################################################################################################
// Constructor
// ##############################################################################################################################
#region Constructor
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += _OnLoaded;
}
private void _OnLoaded(object sender, RoutedEventArgs e)
{
//var children = VisualTree.GetVisualChildCollection<Border>(sender);
if (sender is Control control)
{
Border border = VisualTree.GetVisualChild<Border>(control);
if(ReadLocalValue(CornerRadiusProperty) != DependencyProperty.UnsetValue)
border.CornerRadius = CornerRadius;
}
}
#endregion
}
<Expander>
<i:Interaction.Behaviors>
<zls:ModifyBorderBehavior CornerRadius="0"/>
</i:Interaction.Behaviors>
</Expander>
Can anyone show me a simple working example for a WPF MVVM application to set the ItemsSource of combobox B based on the SelectedItem of ComboBox A?
It seems from what I've found on this site that it gets all too complicated all too quickly.
What's the "right" MVVM way to get it done?
Thank you.
EDIT
I updated using Didier's example.
An extract of my XAML:
<ComboBox Name="BrowserStackDesktopOS" ItemsSource="Binding Platforms.AvailableBrowserStackDesktopOSes}" SelectedIndex="0" SelectedItem="{Binding Platforms.BrowserStackDesktopOSSelectedValue, Mode=TwoWay}"/>
<ComboBox Name="BrowserStackDesktopOSVersion" ItemsSource="{Binding Platforms.AvailableBrowserStackDesktopOSVersions}" SelectedIndex="0" SelectedItem="{Binding Platforms.BrowserStackDesktopOSVersionSelectedValue, Mode=TwoWay}"/>
<ComboBox Name="BrowserStackDesktopBrowser" ItemsSource="{Binding Platforms.AvailableBrowserStackDesktopBrowsers}" SelectedIndex="0" SelectedItem="{Binding Platforms.BrowserStackDesktopBrowserSelectedValue, Mode=TwoWay}"/>
<ComboBox Name="BrowserStackDesktopBrowserVersion" ItemsSource="{Binding Platforms.AvailableBrowserStackDesktopBrowserVersions}" SelectedIndex="0" SelectedItem="{Binding Platforms.BrowserStackDesktopBrowserVersionSelectedValue, Mode=TwoWay}"/>
And an example of my code behind:
public string BrowserStackDesktopOSSelectedValue {
get { return (string)GetValue(BrowserStackDesktopOSSelectedValueProperty); }
set { SetValue(BrowserStackDesktopOSSelectedValueProperty, value);
AvailableBrowserStackDesktopOSVersions = AvailableBrowserStackDesktopPlatforms.GetOSVersions(BrowserStackDesktopOSSelectedValue);
NotifyPropertyChanged("BrowserStackDesktopOSSelectedValue");
}
}
However when I select a value for the first ComboBox nothing happens. I am wanting the Itemsource of the next ComboBox to by populated.
What have I done wrong?
Basically you need to expose in your MVVM 2 collections of values for combo-box choices and two properties for selected values.
In the beginning only the first collection if filled with values. When the first selected value changes the second collection will be filled in with appropriate values. Here is an example implementation:
Code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
//Set the data context of the window
DataContext = new TestVM();
}
}
public class TestVM : INotifyPropertyChanged
{
#region Class attributes
protected static string[] firstComboValues = new string[] { "Choice_1", "Choice_2" };
protected static string[][] secondComboValues =
new string[][] {
new string[] { "value_1_1", "value_1_2", "value_1_3" },
new string[] { "value_2_1", "value_2_2", "value_2_3" }
};
#endregion
#region Public Properties
#region FirstSelectedValue
protected string m_FirstSelectedValue;
/// <summary>
///
/// </summary>
public string FirstSelectedValue
{
get { return m_FirstSelectedValue; }
set
{
if (m_FirstSelectedValue != value)
{
m_FirstSelectedValue = value;
UpdateSecondComboValues();
NotifyPropertyChanged("FirstSelectedValue");
}
}
}
#endregion
#region SecondSelectedValue
protected string m_SecondSelectedValue;
/// <summary>
///
/// </summary>
public string SecondSelectedValue
{
get { return m_SecondSelectedValue; }
set
{
if (m_SecondSelectedValue != value)
{
m_SecondSelectedValue = value;
NotifyPropertyChanged("SecondSelectedValue");
}
}
}
#endregion
#region FirstComboValues
protected ObservableCollection<string> m_FirstComboValues;
/// <summary>
///
/// </summary>
public ObservableCollection<string> FirstComboValues
{
get { return m_FirstComboValues; }
set
{
if (m_FirstComboValues != value)
{
m_FirstComboValues = value;
NotifyPropertyChanged("FirstComboValues");
}
}
}
#endregion
#region SecondComboValues
protected ObservableCollection<string> m_SecondComboValues;
/// <summary>
///
/// </summary>
public ObservableCollection<string> SecondComboValues
{
get { return m_SecondComboValues; }
set
{
if (m_SecondComboValues != value)
{
m_SecondComboValues = value;
NotifyPropertyChanged("SecondComboValues");
}
}
}
#endregion
#endregion
public TestVM()
{
FirstComboValues = new ObservableCollection<string>(firstComboValues);
}
/// <summary>
/// Update the collection of values for the second combo box
/// </summary>
protected void UpdateSecondComboValues()
{
int firstComboChoice;
for (firstComboChoice = 0; firstComboChoice < firstComboValues.Length; firstComboChoice++)
{
if (firstComboValues[firstComboChoice] == FirstSelectedValue)
break;
}
if (firstComboChoice == firstComboValues.Length)// just in case of a bug
SecondComboValues = null;
else
SecondComboValues = new ObservableCollection<string>(secondComboValues[firstComboChoice]);
}
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
And the associated XAML
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="window" x:Class="Testing1.MainWindow">
<Grid>
<Grid HorizontalAlignment="Center" VerticalAlignment="Center" Width=" 300">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="10"/>
<RowDefinition/>
</Grid.RowDefinitions>
<ComboBox x:Name="FirstOne" ItemsSource="{Binding FirstComboValues}" SelectedItem="{Binding FirstSelectedValue, Mode=TwoWay}"/>
<ComboBox x:Name="SecondOne" ItemsSource="{Binding SecondComboValues}" SelectedItem="{Binding SecondSelectedValue, Mode=TwoWay}" Grid.Row="2"/>
</Grid>
</Grid>
</Window>
As you can see the SelectedValue properties of combo boxes are binded in TwoWay mode so when SelectedValue property of the combo box changes it changes the value on the VM side. And in FirstSelectedValue property setter UpdateSecondComboValues() method is called to update values for the second combo box.
EDIT:
It happens because you mixed both INotifPropertyChanged and DependencyObject. You should choose one of them. Usually you implement INotifyPropertyChanged in your VM and the code in the property setter will work.
If you inherit from DependencyObject however, you should not write any code in the setter/getter. It will never be called by the TwoWay binding. It will just call GetValue(...) internally. To be able to execute an action on DependencyProperty change you should declare it differently with a property changed handler:
#region BrowserStackDesktopOSSelectedValue
/// <summary>
/// BrowserStackDesktopOSSelectedValue Dependency Property
/// </summary>
public static readonly DependencyProperty BrowserStackDesktopOSSelectedValue Property =
DependencyProperty.Register("BrowserStackDesktopOSSelectedValue ", typeof(string), typeof(YourVM),
new FrameworkPropertyMetadata((string)null,
new PropertyChangedCallback(OnBrowserStackDesktopOSSelectedValue Changed)));
/// <summary>
/// Gets or sets the BrowserStackDesktopOSSelectedValue property. This dependency property
/// indicates ....
/// </summary>
public string BrowserStackDesktopOSSelectedValue
{
get { return (string)GetValue(BrowserStackDesktopOSSelectedValue Property); }
set { SetValue(BrowserStackDesktopOSSelectedValue Property, value); }
}
/// <summary>
/// Handles changes to the BrowserStackDesktopOSSelectedValue property.
/// </summary>
private static void OnBrowserStackDesktopOSSelectedValue Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
YourVM target = (YourVM)d;
string oldBrowserStackDesktopOSSelectedValue = (string)e.OldValue;
string newBrowserStackDesktopOSSelectedValue = target.BrowserStackDesktopOSSelectedValue ;
target.OnBrowserStackDesktopOSSelectedValue Changed(oldBrowserStackDesktopOSSelectedValue , newBrowserStackDesktopOSSelectedValue );
}
/// <summary>
/// Provides derived classes an opportunity to handle changes to the BrowserStackDesktopOSSelectedValue property.
/// </summary>
protected virtual void OnBrowserStackDesktopOSSelectedValue Changed(string oldBrowserStackDesktopOSSelectedValue , string newBrowserStackDesktopOSSelectedValue )
{
//Here write some code to update your second ComboBox content.
AvailableBrowserStackDesktopOSVersions = AvailableBrowserStackDesktopPlatforms.GetOSVersions(BrowserStackDesktopOSSelectedValue);
}
#endregion
By the way I always use Dr WPF snippets to write DPs so it goes much faster.
Say I have the following small UserControl:
<StackPanel>
<TextBlock Text="{Binding Title, StringFormat=Name: {0}}" FontWeight="Bold"/>
<Separator/>
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=. ,StringFormat=Detail: {0}}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Line Stroke="Black" HorizontalAlignment="Stretch" Margin="0,10"
X2="{Binding ActualWidth, RelativeSource={RelativeSource Self}}"
StrokeDashArray="2 2" StrokeThickness="1" />
</StackPanel>
My application will generate a collection of these controls and then they should be printed with automatic paginating. The number of items that is shown in the ItemsControl is variable. I scourged the Net, read the chapter about printing in Pro WPF 4.5 Unleashed, but still I do not see how to accomplish this.
Any guidance is very welcome.
EDIT: any guidance is welcome. As I have the data available in a view model it will not be too difficult to redirect its data elsewhere. But where?
Since you require automatic pagination, you'll need to create a DocumentPaginator object.
The following is stolen from this example and modified for your case:
/// <summary>
/// Document paginator.
/// </summary>
public class UserControlDocumentPaginator : DocumentPaginator
{
private readonly UserControlItem[] userControlItems;
private Size pageSize;
private int pageCount;
private int maxRowsPerPage;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="userControlItems">The list of userControl items to display.</param>
/// <param name="pageSize">The size of the page in pixels.</param>
public UserControlDocumentPaginator
(
UserControlItem[] userControlItems,
Size pageSize
)
{
this.userControlItems = userControlItems;
this.pageSize = pageSize;
PaginateUserControlItems();
}
/// <summary>
/// Computes the page count based on the number of userControl items
/// and the page size.
/// </summary>
private void PaginateUserControlItems()
{
double actualHeight;
foreach (var uc in userControlItems)
{
actualHeight += uc.ActualHeight;
}
pageCount = (int)Math.Ceiling(actualHeight / pageSize.Height);
}
/// <summary>
/// Gets a range of userControl items from an array.
/// </summary>
/// <param name="array">The userControl items array.</param>
/// <param name="start">Start index.</param>
/// <param name="end">End index.</param>
/// <returns></returns>
private static UserControlItem[] GetRange(UserControlItem[] array, int start, int end)
{
List<UserControlItem> userControlItems = new List<UserControlItem>();
for (int i = start; i < end; i++)
{
if (i >= array.Count())
{
break;
}
userControlItems.Add(array[i]);
}
return userControlItems.ToArray();
}
#region DocumentPaginator Members
/// <summary>
/// When overridden in a derived class, gets the DocumentPage for the
/// specified page number.
/// </summary>
/// <param name="pageNumber">
/// The zero-based page number of the document page that is needed.
/// </param>
/// <returns>
/// The DocumentPage for the specified pageNumber, or DocumentPage.Missing
/// if the page does not exist.
/// </returns>
public override DocumentPage GetPage(int pageNumber)
{
// Compute the range of userControl items to display
int start = pageNumber * maxRowsPerPage;
int end = start + maxRowsPerPage;
UserControlListPage page = new UserControlListPage(GetRange(userControlItems, start, end), pageSize);
page.Measure(pageSize);
page.Arrange(new Rect(pageSize));
return new DocumentPage(page);
}
/// <summary>
/// When overridden in a derived class, gets a value indicating whether
/// PageCount is the total number of pages.
/// </summary>
public override bool IsPageCountValid
{
get { return true; }
}
/// <summary>
/// When overridden in a derived class, gets a count of the number of
/// pages currently formatted.
/// </summary>
public override int PageCount
{
get { return pageCount; }
}
/// <summary>
/// When overridden in a derived class, gets or sets the suggested width
/// and height of each page.
/// </summary>
public override System.Windows.Size PageSize
{
get
{
return pageSize;
}
set
{
if (pageSize.Equals(value) != true)
{
pageSize = value;
PaginateUserControlItems();
}
}
}
/// <summary>
/// When overridden in a derived class, returns the element being paginated.
/// </summary>
public override IDocumentPaginatorSource Source
{
get { return null; }
}
#endregion
}
Then in your code behind for your Window, get a list of your user controls (replace YourUserControlContainer with the name of the container in your Window). Create a button named PrintButton and attach the Click event to the PrintButton_Click method below. Put in code behind where appropriate:
List<UserControl> userControlItems = new List<UserControl>();
// 8.5 x 11 paper
Size pageSize = new Size(816, 1056);
private void PrintButton_Click(object sender, RoutedEventArgs e)
{
userControlItems = YourUserControlContainer.Children.ToList();
UserControlDocumentPaginator paginator = new UserControlDocumentPaginator
(
userControlItems.ToArray(),
pageSize
);
var dialog = new PrintDialog();
if (dialog.ShowDialog() != true) return;
dialog.PrintDocument(paginator, "Custom Paginator Print Job");
}
Edit You're right, I forgot a class. You'll need something like this:
public partial class UserControlListPage : UserControl
{
private readonly UserControlItem[] userControlItems;
private readonly Size pageSize;
public UserControlListPage
(
UserControlItem[] userControlItems,
Size pageSize
)
{
InitializeComponent();
this.userControlItems = userControlItems;
this.pageSize = pageSize;
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
Point point = new Point(0, 0);
foreach (UserControlItem item in userControlItems)
{
point.X = 0;
itemImageSource = CopyControlToImageSource(item);
drawingContext.DrawImage(itemImageSource, point);
point.Y += itemImageSource.Height;
}
}
}
Then somewhere put this:
/// <summary>
/// Gets an image "screenshot" of the specified UIElement
/// </summary>
/// <param name="source">UIElement to screenshot</param>
/// <param name="scale" value="1">Scale to render the screenshot</param>
/// <returns>Byte array of BMP data</returns>
private static byte[] GetUIElementSnapshot(UIElement source, double scale = 1)
{
double actualHeight = source.RenderSize.Height;
double actualWidth = source.RenderSize.Width;
double renderHeight = actualHeight * scale;
double renderWidth = actualWidth * scale;
RenderTargetBitmap renderTarget = new RenderTargetBitmap((int)renderWidth, (int)renderHeight, 96, 96, PixelFormats.Pbgra32);
VisualBrush sourceBrush = new VisualBrush(source);
DrawingVisual drawingVisual = new DrawingVisual();
DrawingContext drawingContext = drawingVisual.RenderOpen();
using (drawingContext)
{
drawingContext.PushTransform(new ScaleTransform(scale, scale));
drawingContext.DrawRectangle(sourceBrush, null, new Rect(new System.Windows.Point(0, 0), new System.Windows.Point(actualWidth, actualHeight)));
}
renderTarget.Render(drawingVisual);
Byte[] _imageArray = null;
BmpBitmapEncoder bmpEncoder = new BmpBitmapEncoder();
bmpEncoder.Frames.Add(BitmapFrame.Create(renderTarget));
using (MemoryStream outputStream = new MemoryStream())
{
bmpEncoder.Save(outputStream);
_imageArray = outputStream.ToArray();
}
return _imageArray;
}
public static System.Windows.Media.Imaging.BitmapImage CopyControlToImageSource(UIElement UserControl)
{
ImageConverter ic = new ImageConverter();
System.Drawing.Image img = (System.Drawing.Image)ic.ConvertFrom(GetUIElementSnapshot(UserControl));
MemoryStream ms = new MemoryStream();
img.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp);
System.Windows.Media.Imaging.BitmapImage image = new BitmapImage();
image.BeginInit();
ms.Seek(0, SeekOrigin.Begin);
image.StreamSource = ms;
image.EndInit();
return image;
}
Its a bit difficult to understand where you stuck at exactly since we do not have you code and we do not know about your data, however here is an simple example:
<Window x:Class="WpfApplication1.PrintVisualDemo"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Print Visual Demo" Height="350" Width="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="0.9*" />
<RowDefinition Height="0.2*"/>
</Grid.RowDefinitions>
<StackPanel Name="printPanel"
Grid.Row="0"
Margin="5">
<Label Content="Sweet Baby"
HorizontalAlignment="Center"
FontSize="40"
FontFamily="Calibri">
<Label.Foreground>
<LinearGradientBrush Opacity="1"
StartPoint="0,0.5" EndPoint="1,0.5">
<LinearGradientBrush.GradientStops>
<GradientStop Color="Blue" Offset="0" />
<GradientStop Color="Red" Offset="0.5" />
<GradientStop Color="Green" Offset="1" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Label.Foreground>
</Label>
<Image Source="D:\Temp\baby.jpg"
Height="150"
Width="200">
</Image>
</StackPanel>
<Button Content="Print"
Grid.Row="1" Margin="5"
Height="40" Width="150"
HorizontalAlignment="Center"
VerticalAlignment="Center" Click="Button_Click" />
</Grid>
</Window>
private void Button_Click(object sender, RoutedEventArgs e)
{
PrintDialog pd = new PrintDialog();
if (pd.ShowDialog() != true) return;
pd.PrintVisual(printPanel, "Printing StackPanel...");
}
PrintVisual method should do the job.
If you want to scale your visual to fit the page you could follow those steps:
PrintDialog printDlg = new System.Windows.Controls.PrintDialog();
if (printDlg.ShowDialog() == true)
{
//get selected printer capabilities
System.Printing.PrintCapabilities capabilities = printDlg.PrintQueue.GetPrintCapabilities(printDlg.PrintTicket);
//get scale of the print wrt to screen of WPF visual
double scale = Math.Min(capabilities.PageImageableArea.ExtentWidth / this.ActualWidth, capabilities.PageImageableArea.ExtentHeight /
this.ActualHeight);
//Transform the Visual to scale
this.LayoutTransform = new ScaleTransform(scale, scale);
//get the size of the printer page
Size sz = new Size(capabilities.PageImageableArea.ExtentWidth, capabilities.PageImageableArea.ExtentHeight);
//update the layout of the visual to the printer page size.
this.Measure(sz);
this.Arrange(new Rect(new Point(capabilities.PageImageableArea.OriginWidth, capabilities.PageImageableArea.OriginHeight), sz));
//now print the visual to printer to fit on the one page.
printDlg.PrintVisual(this, "First Fit to Page WPF Print");
}
As you can see all you need to do is remeasure and rearrange the specific element with new size and then you can call PrintVisual method.
I hope this helps you any futher. Consider post is more code or upload your project online somewhere.
WPF Ribbon has poor image quality. I added
<Window.Resources>
<Style TargetType="{x:Type Image}">
<Setter Property="RenderOptions.BitmapScalingMode" Value="HighQuality" />
</Style>
</Window.Resources>
in my ribbon windows - but it doesnt helped. Also tried with
<ribbon:RibbonWindow.Resources>
image quality is still bad :-(
Does anybody know a workaround for this problem? My images are 48x48 png and i used them for large image icons
Thanks
Michael
Have you checked out the following page from Microsoft with specific recommendations on image size / DPI?
http://msdn.microsoft.com/en-us/library/windows/desktop/dd316921(v=vs.85).aspx
The reason for you poor quality, is because of incorrect image size. Small icons must be 16 x 16. And large icons must 32 x 32. If the image size is of another size it will be stretched. And the stretching will result in a reduced image quality.
I had the same problem and I created my own usercontrol to solve this.
This is how I did it:
<ribbon:Ribbon>
<ribbon:RibbonTab Header="File">
<ribbon:RibbonGroup Header="File">
<views:ImageButton Command="{Binding LoadCommand}" Caption="Open" SourceImage="/Images/save.png"/>
</ribbon:RibbonGroup>
</ribbon:RibbonTab>
</ribbon:Ribbon>
The image button usercontrol ImageButton.xaml
<UserControl Name="control"
<Button Command="{Binding Command, ElementName=control}" Style="{x:Null}">
<StackPanel>
<infrastructure:AutoGreyableImage Width="32" Source="{Binding SourceImage, ElementName=control}"/>
<TextBlock Text="{Binding Caption, ElementName=control}"/>
</StackPanel>
</Button>
The image button usercontrol ImageButton.xaml.cs
public partial class ImageButton : UserControl
{
public static DependencyProperty CommandProperty = DependencyProperty.Register(
"Command", typeof(ICommand), typeof(ImageButton));
public static DependencyProperty SourceProperty = DependencyProperty.Register(
"SourceImage", typeof(string), typeof(ImageButton));
public static DependencyProperty CaptionProperty = DependencyProperty.Register(
"Caption", typeof(string), typeof(ImageButton));
public ICommand Command
{
get
{
return (ICommand)GetValue(CommandProperty);
}
set
{
SetValue(CommandProperty, value);
}
}
public string SourceImage
{
get
{
return (string)GetValue(SourceProperty);
}
set
{
SetValue(SourceProperty, value);
}
}
public string Caption
{
get
{
return (string)GetValue(CaptionProperty);
}
set
{
SetValue(CaptionProperty, value);
}
}
public ImageButton()
{
InitializeComponent();
}
}
For the AutogreyableImage, I used this post
This is a copy paste of the class
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
/// <summary>
/// Class used to have an image that is able to be gray when the control is not enabled.
/// Author: Thomas LEBRUN (http://blogs.developpeur.org/tom)
/// </summary>
public class AutoGreyableImage : Image
{
/// <summary>
/// Initializes a new instance of the <see cref="AutoGreyableImage"/> class.
/// </summary>
static AutoGreyableImage()
{
// Override the metadata of the IsEnabled property.
IsEnabledProperty.OverrideMetadata(typeof(AutoGreyableImage), new FrameworkPropertyMetadata(true, new PropertyChangedCallback(OnAutoGreyScaleImageIsEnabledPropertyChanged)));
}
/// <summary>
/// Called when [auto grey scale image is enabled property changed].
/// </summary>
/// <param name="source">The source.</param>
/// <param name="args">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
private static void OnAutoGreyScaleImageIsEnabledPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs args)
{
var autoGreyScaleImg = source as AutoGreyableImage;
var isEnable = Convert.ToBoolean(args.NewValue);
if (autoGreyScaleImg != null)
{
if (!isEnable)
{
// Get the source bitmap
var bitmapImage = new BitmapImage(new Uri(autoGreyScaleImg.Source.ToString()));
// Convert it to Gray
autoGreyScaleImg.Source = new FormatConvertedBitmap(bitmapImage, PixelFormats.Gray32Float, null, 0);
// Create Opacity Mask for greyscale image as FormatConvertedBitmap does not keep transparency info
autoGreyScaleImg.OpacityMask = new ImageBrush(bitmapImage);
}
else
{
// Set the Source property to the original value.
autoGreyScaleImg.Source = ((FormatConvertedBitmap)autoGreyScaleImg.Source).Source;
// Reset the Opcity Mask
autoGreyScaleImg.OpacityMask = null;
}
}
}
}
I hope this will help you and the others coming