I recently implemented an animated Typewriter effect in Silverlight using F#. My code works, but I'd like to see if anyone has recommendations on ways the code might be improved. i.e. Would it be a good or bad idea to do it with Seq.iter or Seq.map? Any opinions and comments are welcome!
I also thought I'd document the code in case it might help anyone else
Feedback Edit: Added local function for hooking animations to the storyboard
let createTypewriter(text:string) =
//Controls the animation of each character
let storyboard = new Storyboard()
//This will contain every line of text
let textboard = new StackPanel(Orientation=Orientation.Vertical)
//Creates a new StackPanel to hold the words and puts it in the wrapPanel
let newWordContainer (wrapPanel:WrapPanel) =
let wordContainer = new StackPanel(Orientation=Orientation.Horizontal)
wrapPanel.Children.Add( wordContainer)
wordContainer
//Parse the entire string
let rec parseLetters letter delay wordContainer wrapPanel : Storyboard * StackPanel =
match letter with
//If there's nothing left to parse return the initialized storyboard
//and textboard
| [] -> (storyboard, textboard)
//Using pattern matching we recursively handle the current character (head)
//then rest of the characters (tail)
//Handle Spaces. If we encounter a space, we create a new horizontal
//StackPanel to put individual characters into. This new StackPanel
//is added to the wrapPanel
| head :: tail when head = ' ' ->
let newCont = newWordContainer wrapPanel
newCont.Children.Add(new TextBlock(Text=" "))
parseLetters tail (delay+1.0) newCont wrapPanel
//If we encounter a newline or a return, we want to move down to a new line.
//Thus we insert a new WrapPanel into our vertical StackPanel (textboard)
| head :: tail when head ='\n' || head = '\r' -> //Handle new lines
let newWrapPanel = new WrapPanel(MinHeight = this.FontSize)
let newCont = newWordContainer newWrapPanel
textboard.Children.Add(newWrapPanel)
parseLetters tail (delay+1.0) newCont newWrapPanel
//Letters will be placed in TextBlocks and added to horizontal StackPanels
//(wordContainer) to make words. Each TextBlock will have an animation
//controlled by the StoryBoard.
| head :: tail ->
//Create the animation transforms
let st = new ScaleTransform(ScaleX=5.0, ScaleY=5.0)
let tt = new TranslateTransform(X=(-40.0),Y=0.0)
let tg = new TransformGroup()
tg.Children.Add(st)
tg.Children.Add(tt)
//Create the TextBlock and set its transform
let tb = new TextBlock(Text=head.ToString(), Opacity=0.0, RenderTransform=tg,RenderTransformOrigin = new Point(0.5,0.5))
wordContainer.Children.Add(tb)
//Create the DoubleAnimations that specify how the text will animate,
//Darn Nullable types make this really ugly =/
let bt = TimeSpan.FromMilliseconds(1000.0 + delay * 30.0);
let duration = new Duration(TimeSpan.FromSeconds(0.1))
let opacityDA = new DoubleAnimation(From=Nullable(0.0), To=Nullable(1.0), Duration=duration, BeginTime=Nullable(bt))
let translateDA = new DoubleAnimation( From=Nullable(-40.0), To=Nullable(0.0), Duration=duration, BeginTime=Nullable(bt))
let scaleXDA = new DoubleAnimation(From=Nullable(5.0), To=Nullable(1.0), Duration=duration, BeginTime=Nullable(bt))
let scaleYDA = new DoubleAnimation(From=Nullable(5.0), To=Nullable(1.0), Duration=duration, BeginTime=Nullable(bt))
//Create a function that will hook the animation info to the storyboard.
let addToStoryboard doubleAni obj (propName:string) =
Storyboard.SetTarget(doubleAni, obj)
Storyboard.SetTargetProperty(doubleAni, new PropertyPath(propName))
storyboard.Children.Add(doubleAni)
addToStoryboard scaleXDA st "ScaleX"
addToStoryboard scaleYDA st "ScaleY"
addToStoryboard translateDA tt "X"
addToStoryboard opacityDA tb "Opacity"
//Parse the rest of the letters
parseLetters tail (delay+1.0) wordContainer wrapPanel
//Begin the recursion over the passed in string
let wrapPanel = new WrapPanel()
textboard.Children.Add(wrapPanel)
parseLetters (Seq.toList text) 0.0 (newWordContainer wrapPanel) wrapPanel
The only thing that jumps out at me is the repeated StoryBoard code. It probably makes sense to create a local let function definition taking a StoryBoard, DoubleAnimation, string, and obj and condensing the logic for the ScaleX, ScaleY, X, and Opacity animations to one call each.
Related
I need to show/hide some data points in oxyplot line series. Is it possible?
Though some markers are invisible, the line should go through the invisible markers.
You could make use of two series to achieve this. The first one would draw the complete points (and line) without the marker. The second series would draw the visible points(with marker,but with line style set to none). For Example
DataPoint[] points = new DataPoint[]
{
new DataPoint(1,12),
new DataPoint(2,10),
new DataPoint(3,9),
new DataPoint(4,13),
new DataPoint(5,14),
new DataPoint(6,10)
};
var seriesComplete = new OxyPlot.Series.LineSeries();
seriesComplete.Points.AddRange(points);
var seriesVisible = new OxyPlot.Series.LineSeries();
seriesVisible.Points.AddRange(points.Where(x => x.Y % 2 == 0));
seriesVisible.MarkerFill = OxyColors.Blue;
seriesVisible.MarkerType = MarkerType.Circle;
seriesVisible.MarkerSize = 10;
seriesVisible.LineStyle = LineStyle.None;
this.MyModel.Series.Add(seriesComplete);
this.MyModel.Series.Add(seriesVisible);
Result is attached as image
I want to combine 2 curves like this:
Then here is my code:
// Create a path to draw a geometry with.
Path myPath = new Path();
myPath.Stroke = Brushes.Black;
myPath.StrokeThickness = 1;
var gmy1 = (StreamGeometry)StreamGeometry.Parse("M100,100C110,118.333333333333 138.333333333333,206.666666666667 160,210 181.666666666667,213.333333333333 205,123.333333333333 230,120 255,116.666666666667 280,186.666666666667 310,190 340,193.333333333333 396.666666666667,156.666666666667 410,140 423.333333333333,123.333333333333 393.333333333333,98.3333333333333 390,90");
var gmy2 = (StreamGeometry)StreamGeometry.Parse("M180,241.25L180,241.25 230,290 300,246.66667175293 330,160");
var gmy = Geometry.Combine(gmy1, gmy2, GeometryCombineMode.Union, null);
myPath.Data = gmy;
// Add path shape to the UI.
this.panel1.Children.Add(myPath);
But the result is this:
How to combine the curves in WPF?
And because of the project limitation, we have to implement this without layout and xaml. That means we need the result type is Geometry.
More general than concatenating path strings:
If you have a set of arbitrary Geometries and want to group them, use a GeometryGroup:
Geometry gmy1 = ...;
Geometry gmy2 = ...;
var gmy = new GeometryGroup();
gmy.Children.Add(gmy1);
gmy.Children.Add(gmy2);
myPath.Data = gmy;
Easy:
Path myPath = new Path();
myPath.Stroke = Brushes.Black;
myPath.StrokeThickness = 1;
var gmy1 = (StreamGeometry)StreamGeometry.Parse("M100,100C110,118.333333333333 138.333333333333,206.666666666667 160,210 181.666666666667,213.333333333333 205,123.333333333333 230,120 255,116.666666666667 280,186.666666666667 310,190 340,193.333333333333 396.666666666667,156.666666666667 410,140 423.333333333333,123.333333333333 393.333333333333,98.3333333333333 390,90");
var gmy2 = (StreamGeometry)StreamGeometry.Parse("M180,241.25L180,241.25 230,290 300,246.66667175293 330,160");
var gmy = (StreamGeometry)StreamGeometry.Parse(gmy1.ToString() + gmy2.ToString());
myPath.Data = gmy;
// Add path shape to the UI.
this.panel1.Children.Add(myPath);
The path definition language is a language. Use it as one. StreamGeometry.ToString() unparses a Geometry back to its Path Definition Language representation, which you can then merge with another one.
Note that this works because each starts with a M for Move command: It starts a new line. I don't think there's any realistic case where you'd run into any trouble with that (and it won't let you start with L for Line), but theory's not exactly my strongest subject.
Just add both of them to a Grid or Canvas, Combine does a intersecting combination, you just seem to want to overlay them. Alternatively add both of them to a GeometryGroup and add that to your panel.
I create FixedDocument in more iterations (one page per iteration) like this:
PrintDialog pr = new PrintDialog();
FixedDocument doc = new FixedDocument();
foreach(var i in a)
{
// some changes of MaingGrid here
...
//
VisualBrush vb = new VisualBrush(this.MainGrid);
FixedPage page = new FixedPage();
page.Width = doc.DocumentPaginator.PageSize.Width;
page.Height = doc.DocumentPaginator.PageSize.Height;
Rectangle rec = new Rectangle();
rec.Width = this.MainGrid.ActualWidth;
rec.Height = this.MainGrid.ActualHeight;
rec.Fill = vb;
page.Children.Add(rec);
PageContent content = new PageContent();
((IAddChild)content).AddChild(page);
doc.Pages.Add(content);
}
pr.PrintDocument(doc.DocumentPaginator, "test");
In each iteration I change the MainGrid a little. So each page should contain the actual state of MainGrid. But the printed document contains pages with same content of last iteration (in other words - the last state is on all pages in document). Is there any "lazy evaluation" of VisualBrush or something?
Call .Freeze() on the VisualBrush in each iteration. Otherwise, it will always be a live view of whatever visual you pointed it at.
EDIT: Freeze doesn't work but you can render the brush into a static bitmap. See http://blog.avanadeadvisor.com/blogs/markti/archive/2008/04/14/10888.aspx
My target: a DocumentPaginator which takes a FlowDocument with a table, which splits the table to fit the pagesize and repeat the header/footer (special tagged TableRowGroups) on every page.
For splitting the table I have to know the heights of its rows.
While building the FlowDocument-table by code, the height/width of the TableRows are 0 (of course). If I assign this document to a FlowDocumentScrollViewer (PageSize is set), the heights etc. are calculated. Is this possible without using an UI-bound object? Instantiating a FlowDocumentScrollViewer which is not bound to a window doesn't force the pagination/calculation of the heights.
This is how I determine the height of a TableRow (which works perfectly for documents shown by a FlowDocumentScrollViewer):
FlowDocument doc = BuildNewDocument();
// what is the scrollviewer doing with the FlowDocument?
FlowDocumentScrollViewer dv = new FlowDocumentScrollViewer();
dv.Document = doc;
dv.Arrange(new Rect(0, 0, 0, 0));
TableRowGroup dataRows = null;
foreach (Block b in doc.Blocks)
{
if (b is Table)
{
Table t = b as Table;
foreach (TableRowGroup g in t.RowGroups)
{
if ((g.Tag is String) && ((String)g.Tag == "dataRows"))
{
dataRows = g;
break;
}
}
}
if (dataRows != null)
break;
}
if (dataRows != null)
{
foreach (TableRow r in dataRows.Rows)
{
double maxCellHeight = 0.0;
foreach (TableCell c in r.Cells)
{
Rect start = c.ElementStart.GetCharacterRect(LogicalDirection.Forward);
Rect end = c.ElementEnd.GetNextInsertionPosition(LogicalDirection.Backward).GetCharacterRect(LogicalDirection.Forward);
double cellHeight = end.Bottom - start.Top;
if (cellHeight > maxCellHeight)
maxCellHeight = cellHeight;
}
System.Diagnostics.Trace.WriteLine("row " + dataRows.Rows.IndexOf(r) + " = " + maxCellHeight);
}
}
Edit:
I added the FlowDocumentScrollViewer to my example. The call of "Arrange" forces the FlowDocument to calculate its heights etc. I would like to know, what the FlowDocumentScrollViewer is doing with the FlowDocument, so I can do it without the UIElement. Is it possible?
My guess would be no, you can't do it without a UIElement.
FlowDocument, by itself, doesn't actually render anything. Looking at the type in relector it looks like it is just a data type. Its about like having a string and wanting to know its size when rendered ... can't really do it without doing some kind of measure pass.
I don't know for sure, but you might get better performance in the arrange pass by passing in Double.PositiveInfinity for the size rather than 0. At least then it won't have to worry about measuring 'n' line breaks.
I'm building a demo app in WPF, which is new to me. I'm currently displaying text in a FlowDocument, and need to print it.
The code I'm using looks like this:
PrintDialog pd = new PrintDialog();
fd.PageHeight = pd.PrintableAreaHeight;
fd.PageWidth = pd.PrintableAreaWidth;
fd.PagePadding = new Thickness(50);
fd.ColumnGap = 0;
fd.ColumnWidth = pd.PrintableAreaWidth;
IDocumentPaginatorSource dps = fd;
pd.PrintDocument(dps.DocumentPaginator, "flow doc");
fd is my FlowDocument, and for now I'm using the default printer instead of allowing the user to specify print options. It works OK, except that after the document prints, the FlowDocument displayed on screen has changed to to use the settings I specified for printing.
I can fix this by manually resetting everything after I print, but is this the best way? Should I make a copy of the FlowDocument before I print it? Or is there another approach I should consider?
yes, make a copy of the FlowDocument before printing it. This is because the pagination and margins will be different. This works for me.
private void DoThePrint(System.Windows.Documents.FlowDocument document)
{
// Clone the source document's content into a new FlowDocument.
// This is because the pagination for the printer needs to be
// done differently than the pagination for the displayed page.
// We print the copy, rather that the original FlowDocument.
System.IO.MemoryStream s = new System.IO.MemoryStream();
TextRange source = new TextRange(document.ContentStart, document.ContentEnd);
source.Save(s, DataFormats.Xaml);
FlowDocument copy = new FlowDocument();
TextRange dest = new TextRange(copy.ContentStart, copy.ContentEnd);
dest.Load(s, DataFormats.Xaml);
// Create a XpsDocumentWriter object, implicitly opening a Windows common print dialog,
// and allowing the user to select a printer.
// get information about the dimensions of the seleted printer+media.
System.Printing.PrintDocumentImageableArea ia = null;
System.Windows.Xps.XpsDocumentWriter docWriter = System.Printing.PrintQueue.CreateXpsDocumentWriter(ref ia);
if (docWriter != null && ia != null)
{
DocumentPaginator paginator = ((IDocumentPaginatorSource)copy).DocumentPaginator;
// Change the PageSize and PagePadding for the document to match the CanvasSize for the printer device.
paginator.PageSize = new Size(ia.MediaSizeWidth, ia.MediaSizeHeight);
Thickness t = new Thickness(72); // copy.PagePadding;
copy.PagePadding = new Thickness(
Math.Max(ia.OriginWidth, t.Left),
Math.Max(ia.OriginHeight, t.Top),
Math.Max(ia.MediaSizeWidth - (ia.OriginWidth + ia.ExtentWidth), t.Right),
Math.Max(ia.MediaSizeHeight - (ia.OriginHeight + ia.ExtentHeight), t.Bottom));
copy.ColumnWidth = double.PositiveInfinity;
//copy.PageWidth = 528; // allow the page to be the natural with of the output device
// Send content to the printer.
docWriter.Write(paginator);
}
}
You can use the code from the URL below, it wraps the flow document in a fixed document and prints that, the big advantage is that you can use it to add margin, headers and footers.
https://web.archive.org/web/20150502085246/http://blogs.msdn.com:80/b/fyuan/archive/2007/03/10/convert-xaml-flow-document-to-xps-with-style-multiple-page-page-size-header-margin.aspx
The following works with both text and non-text visuals:
//Clone the source document
var str = XamlWriter.Save(FlowDoc);
var stringReader = new System.IO.StringReader(str);
var xmlReader = XmlReader.Create(stringReader);
var CloneDoc = XamlReader.Load(xmlReader) as FlowDocument;
//Now print using PrintDialog
var pd = new PrintDialog();
if (pd.ShowDialog().Value)
{
CloneDoc.PageHeight = pd.PrintableAreaHeight;
CloneDoc.PageWidth = pd.PrintableAreaWidth;
IDocumentPaginatorSource idocument = CloneDoc as IDocumentPaginatorSource;
pd.PrintDocument(idocument.DocumentPaginator, "Printing FlowDocument");
}
I am also generating a WPF report off a Flow document, but I am purposely using the flow document as a print preview screen. I there for want the margins to be the same. You can read about how I did this here.
In your scenario I'm thinking why not just make a copy of your settings, instead of the entire flow document. You can then re-apply the settings if you wish to return the document back to it's original state.