I want to implement a rather complex CurveEditor that has to support the usual requirements like:
freely scalable and moveable axis
different interpolation types per curve point (Linear, Cubic, Spline)
Tangents (joined and broken)
Selecting one or several points to edit (move, scale, delete) via Fence or Click
Only show handles and highlights for selected curve points
I don't want to manipulate actual WPF curves but an existing model with key/value/tangents sets and sample the precise shape of the curve from our implementation.
I already gathered some experience on implementing custom UserControls and Templates. But I want to make sure, I don't miss any apparent solution. I planned to have this general XAML-tree:
CurveEditor - Window holding all content
MainThumb : Enable dragging and scaling the editor range
XAxis : UserControl rending some scale on the left side
YAxis : UserControl rending some scale on the bottom
Curves : Canvas holding the curves
Curve : UserControl for a single curve
CurveName - Label of the curve
CurveLine - DrawingVisual that will render the actual curve by sampling the internal implementation of the spline function.
CurveEditPoints - Canvas that holds all edit points
CurveEditPoint - UserControl for a single edit point
LeftTangent - UserControl for the left tangent handle
LeftTangentThumb - For modifying the handle
RightTangent - UserControl for the right tangent handle
RightTangentThumb - For modifying the handle
CurvePointCenter - Visualisation of the actual point, select state and interpolation type.
CurvePointThumb - Thumb to select and drag point around
I know, this is quite a complex question and I am not asking for an actual implementation. I am interested in the following questions:
Can you recommend any tutorials or books that might help me (I already got Illustrated WPF, WPF Control Development Unleashed, and a couple of other)
Should minor elements like the Tangents be individual UserControls?
What container is best suited for hosting the individual "Curves", "EditPoints" and "Tangents". Right now, I use Canvas and Canvas.SetLeft/SetTop to position the children, but that feels "strange".
Should I use "Shapes" like Path or DrawingVisual-Classes to implement actual representation. Path is straight forward, but I am concerned about performance with hundreds of CurvePoints.
Should I use Transforms to rotate the tangents or is just fine to do some triangulation math in the code behind files?
Does the structure roughly make sense, or do you suggest a completely different approach?
you seem to have the right tools at hand, WPF Unleashed is excellent, but I guess you have that one already.
make individual UserControls in one of these cases:
you are using the same xaml all over the place (DRY)
you xaml file gets too big (get some components out)
you need to inherit from some class
this depends on how much codebehind you want.
you can, as you suggested in your comment, use an ItemsControl as a container for wherever you need selection between multiple items. so this could also be done on the level of Curves, not just on the level of points on the curve. Depending on how you want to handle drawing of the actual lines and curves you can even have an ItemsControl for those. (on a side note: you will not have virtualization out of the box though, as your items won't have a constant height)
Path is OK with hundreds of CurvePoints. If you have 10.000, I'd say you could get problems.
can't imagine how a transform should be used here, maybe inside an Adorner.
your structure looks good. you will be able to implement all of this. I will suggest though how I would do it:
first of all use MVVM.
CurveEditor
ListBox(Panel=Canvas)(ItemsSource=Curves)(ItemTemplate=CurveControl)
CurveControl
Canvas(Background=Transparent) <= I'm not sure if standard is white, but you don't want to overlap other Curves...
CurveName
ListBox(Panel=Canvas(Background=Transparent))(ItemsSource=CurveParts)
ListBox(Panel=Canvas(Background=Transparent))(ItemsSource=CurvePoints)(ItemTemplate=>EditPointControl)
EditPointControl
Canvas
Thumb(Template = Ellipse) (Name=CenterHandle) (with some Visualstates for Selection and hiding of Tangents)
Thumb(Template = Ellipse) (Name=LeftHandle)
Thumb(Template = Ellipse) (Name=RightHandle)
Line (Binding X/Y to Centerpoint and LeftHandlePoint)
Line (Binding X/Y to Centerpoint and RightHandlePoint)
I have stated to set ItemTemplate for the ListBox. You can however style the listbox however you want (I think the standard style includes a border, you might want to remove that or set bordersize=0) and set instead of ItemTemplate the ItemContainerStyle and bind to IsSelected so you have control over IsSelected from your ViewModel (look here for what I mean).
So the viewmodel looks like this:
- CurveEditorViewModel
- ObservableCollection<CurveViewModel> Curves
- CurveViewModel
- string Label
- (Point LabelPlacement)
- bool IsSelected
- ObservableCollection<CurvePointViewModel> CurvePoints
- ObservableCollection<CurvePartViewModel> CurveParts
- CurvePointViewModel
- Point Position
- bool IsSelected
- Point LeftHandle
- Point RightHandle
- CurvePartViewModel
- CurvePointViewModel StartPoint
- CurvePointViewModel EndPoint
- Path CurvePath
in here you can subscribe to CurvePointViewModel's PropertyChanged and recalculate the Path you're exposing.
I'd probably improve on it as I go but that'd be my first guess.
There are some details you might want to watch out for. eg: the style for the thumbs might be a visible circle in the middle and an invisible bigger one around that with background=transparent. that way you can have your visible circle small, but have the user use the tumb in an area around the small circle.
EDIT:
here is an Example for the Thumb:
<Thumb Width="8" Height="8" Cursor="Hand" Margin="-4">
<Thumb.Template>
<ControlTemplate TargetType="Thumb">
<Grid>
<Ellipse Fill="Transparent" Margin="-6"/>
<Ellipse Stroke="Red" StrokeThickness="2"/>
</Grid>
</ControlTemplate>
</Thumb.Template>
</Thumb>
as you want to position this at a specific point on a canvas setting the Margin to minus half the width and height will place the center of the circle on that point. Furthermore, having that inner ellipse with a transparent fill and Margin of -6 you will get a 6px bigger radius around the inner (smaller) circle where you can drag the thumb.
Related
So there's this tutorial about creating a diagram in WPF.
http://www.codeproject.com/Articles/24681/WPF-Diagram-Designer-Part
I've read it, and still studying it to understand it completely.
At the end of this tutorial, you can basically add shapes, move/rotate/scale them, and since they are created in a vector form, they are keeping their resolutions, there are also connectors that can connect each shape with another.
My goal, since I need to create a simulator which shows how internet protocols are delivered, is to create a divided diagram in which Side A communicates with Side B. it could read an automata and simulate the transitions in the diagram.
I'm thinking of how to deliever this, and since I don't have a lot of knowledge in WPF, I wonder in which way should I implement it.
Should I create 2 different Canvases? or maybe dividing 1 canvas with two sides?
The main issue I'm dealing with, is that when a shape is being dragged to the end margin of the window, then the window allow me to slide it so I can see the rest of the field, this is being done by increasing the size of the Canvas, as seen in the Tutorial Part 1.
However, if my canvas is divided by two, and there's a border in the middle, how can I create two sliders for each of the sides?
I was wondering if you can give me any tips about how approaching this idea, since my knowledge in WPF is still very limited.
Here is my point of view, but it would be very useful if you would provide a more/less final sketch of your app. I recommend using Telerik AppMock but paint will also suffice ;).
From what I have understood you should need 3 canvases.
1-st is canvas on the left.
2-nd is canvas on the right.
3-rd is on top of both canvases.
When you want to drag an element, you must set opacity of the clicked element to be a bit transparent and leave it on its place(1st canvas), add copy of dragged element in to the 3rd canvas. When you do leftmousebuttonup(drop dragged item), you have to check where was it dropped and if it was droppend on the 2nd canvas you add it to this canvas. To position element on the canvas you can use Canvas.SetLeft and accordingly SetRight method.
You can put 1st and 2nd canvases into Grid. Even if Canvases will be bigger if Grid, view will be cut only to the size of the Grid.
Moreover, to allow canvases manipulation, add there (to the Grid) a scrollviewer which will Translate Transform the canvases given to their sizes.
Later, try to use MVVM pattern to fill your Canvases with data.
I would also suggest an ObservableColletion of drawable (you can use FrameworkElement as base class) and draggable objects. Different for every Canvas.
Good luck!
I have this problem.
So I have a bunch of data that must be visualized on a canvas (say more than 5000 items). So I draw them as a bunch of vertical rectangles over a horizontal line, some thing like this:
---|--|||||---|---|||---||----|||||||--------
Now, because the canvas is small, I only draw a different amount of rectangles at different zoom level. So if I zoom in more, the line get longer, and more rectangles I can see.
Problem is every time I zoom in, I have to clear the whole canvas, and redaw everything with the new zoom scale. And it is really suck, the drawing is slow and scaling not really nice.
So I wondering is there a way I can achieve a faster drawing, and good zooming (like those vector graph, you can zoom in unlimited)??
Have you tried ScaleTransform Class?
<Canvas.RenderTransform>
<ScaleTransform ScaleX="2" ScaleY="2" />
</Canvas.RenderTransform>
See How to: Scale an Element too. For performance reasons:
Freeze your Freezables.
Update Rather than Replace a RenderTransform
You may be able to update a Transform rather than replacing it as the
value of a RenderTransform property. This is particularly true in
scenarios that involve animation. By updating an existing Transform,
you avoid initiating an unnecessary layout calculation.
Have you looked at the ZoomableCanvas? I haven't used it, but it looks like it's designed to do exactly what you want.
Wpf hit testing is pretty good but the only method I found to extend the hit zone is to put a transparent padding area around your object. I can't find any method to add a transparent area arround a Path object. The path is very thin and I would like to enable hit testing if the user clicks near the path. I can't find any method to extend the path object with a transparent area like the image below :
alt text http://img175.imageshack.us/img175/6741/linepadding.png
I tried to used a partially transparent stroke brush but I ran into the problem described here : How can I draw a "soft" line in WPF (presumably using a LinearGradientBrush)?
I also tried to put an adorner over my line but because of WPF anti-aliasing algorithms, the position is way off when I zoom in my canvas and interfere with other objects hit-testing in a bad way.
Any suggestion to extend the hit testing zone would be highly appreciated.
Thanks,
Kumar
Path.Data is a geometry object. The Geometry class has several methods that can help you hit test with tolerance:
GetFlattenedPathGeometry(Double, ToleranceType)
GetOutlinedPathGeometry(Double, ToleranceType)
GetRenderBounds(Pen, Double, ToleranceType)
I think GetRenderBounds will work best for you.
Once you have the geometry (plus a little width) you can call
geometry.FillContains(Point, Double, ToleranceType)
or
geometry.StrokeContains(Pen, Point, Double, ToleranceType)
Out of all of that you should tune the desired hit from your hit test;
You can wrap the Path inside a transparent Border.
In WPF you can create another path with its geometry databound to the first (using Element Binding), but with transparent brush and increased thickness.
Something more or less like this:
<Path x:Name="backPath" Data="{Binding Data, ElementName=mainPath}" StrokeThickness="10" Stroke="Transparent"/>
<Path x:Name="mainPath" Data="{Binding DataFromViewModel}" StrokeThickness="1" Stroke="Red"/>
Note that the main path comes after in XAML, so that it is rendered on top.
I'm currently creating a MSPaint-like WPF-application and struggling with the implementation of a snappable grid.
The painting of the grid is no problem with a VisualBrush and a Rectangle but the problem is that these lines are then purely for looks and can't be easily changed (for example highlighted when the snapping to a specific line triggered).
My other idea was to have a 2 Canvas solution where 1 Canvas is used for the elements and one Canvas (who is positioned above the other) contains all the grid lines. However I have the feeling that this would mean quite a performance hit.
Are there any other possible ways to implement this kind of functionality?
Efficiency considerations of a two-panel approach vs DrawingContext
I have good news for you: You are wrong about the significant performance hit. Your two-canvas idea is nearly optimal, even if you use individual objects for the grid lines. This is because WPF uses retained-mode rendering: When you create the canvas, everything on it is serialized into a compact structure at native level. This only changes when you change the grid lines in some way, such as changing the grid spacing. At all other times the performance will be indistinguishable from the very fastest possible managed-code methods.
A slight performance increase could be had by using DrawingContext as Nicholas describes.
A simpler and more efficient solution
Perhaps a better way then drawing individual lines on the grid canvas is to use two tiled visual brushes (one horizontal, one vertical) to draw all unhilighted lines, then use Rectangle(s) added in code-behind to hilight the line(s) you are snapping to.
The main advantage of this technique is that your grid can be effectively infinite, so there is no need to calculate the right number of grid lines to draw and then update this every time the window resizes or the zoom changes. You also only have three UIElements involved, plus one more for each grid line that is currently hilighted. It also seems cleaner to me than tracking collections of grid lines.
The reason you want to use two visual brushes is that drawing is more efficient: The brush drawing the vertical lines is stretched to a huge distance (eg double.MaxValue/2) in the vertical direction so the GPU gets only one drawing call per vertical line, the same for the horizontal. Doing a two-way tiling is much less efficient.
Adorner layer
Since you asked about alternatives, another possibility is to use Adorner and AdornerLayer with any of the solutions above rather than stacking your canvas using eg a Grid or containing Canvas. For a Paint-like application this is nice because the adorner layer can be above your graphic layer(s) yet the adorners can still attach to individual items that are being displayed.
You might consider drawing your grid using the DrawingContext inside of OnRender. Drawing this way does not introduce new UIElements into the visual tree, which helps to keep performance up. In some ways, it is similar to what you are currently doing with the VisualBrush, which also does not create new UI elements per copy.
However, since you will actually be individually drawing each line instead of copying the look of a single line, you'll be able to highlight the grid line(s) that participate in snapping without changing the colors of those that do not.
If you are going to go down this route, make sure to have a look into GuidelineSets for positioning your guide lines (more details here), since you'll probably want to have your guide lines snap to the device's pixels so that they draw sharply.
How can you relatively position elements in WPF?
The standard model is to use layout managers for everything, but what if you want to position elements (on a Canvas, for example) simply based on the position of other elements?
For example, you may want one element (say a button) to be attached the side of another (perhaps a panel) independent of the position or layout of that panel.
Anyone that's worked with engineering tools (SolidWorks, AutoCad, etc.) is familiar with this sort of relative positioning.
Forcing everything into layout managers (the different WPF Panels) does not make much sense for certain scenarios, where you don't care that elements are maintained by some parent container and you do not want the other children to be affected by a change in the layout/appearance of each other. Does WPF support this relative positioning model in any way?
Instead of putting (as in your example) a button directly on the canvas, you could put a stackpanel on the canvas, horizontally aligned, and put the two buttons in there.
Like so:
<Canvas>
<StackPanel Canvas.Left="100" Canvas.Top="100" Orientation="Horizontal">
<Button>Button 1</Button><Button>Button 2</Button>
</StackPanel>
</Canvas>
I think that it's quite flexible when you use more than 1 layout in a form, and you can create pretty much any configuration you want.
Good question. As far as I know, we need to have a different custom panel to get this feature. Since WPF is based on Visual Hierarchy there is no way to have this sort of Flat structure for the elements in the platform.
But Here is a trick to do this.
Place your elements in the same position and give relative displacement by using RenderTransform.TranslateTransform. This way your TranslateTransfrom's X and Y will always be relatuve to the other element.