1 /**This file contains the DFL-specific parts of Plot2Kill and is publicly 2 * imported by plot2kill.figure if compiled with -version=dfl. 3 * 4 * Note that since DFL was the first library that Plot2Kill was ported to, 5 * it is considered the native lib and most stuff in this module maps cleanly 6 * and directly to DFL. 7 */ 8 module plot2kill.dflwrapper; 9 10 11 version(dfl) { 12 13 import dfl.all, dfl.internal.utf, dfl.internal.winapi; 14 15 import plot2kill.util; 16 import plot2kill.png; 17 import plot2kill.guiagnosticbase; 18 import plot2kill.figure; 19 20 /**DFL's implementation of a color object.*/ 21 alias dfl.drawing.Color Color; 22 23 /**DFL's implementation of a line drawing object.*/ 24 alias dfl.drawing.Pen Pen; 25 26 /**DFL's implementation of a filled area drawing object.*/ 27 alias dfl.drawing.Brush Brush; 28 29 /**DFL's object for representing a point.*/ 30 alias dfl.drawing.Point Point; 31 32 /**DFL's implementation of a rectangle object.*/ 33 alias dfl.drawing.Rect Rect; 34 35 /**DFL's size object.*/ 36 alias dfl.drawing.Size Size; 37 38 /**DFL's font class.*/ 39 alias dfl.drawing.Font Font; 40 41 /**Get a color in a GUI framework-agnostic way.*/ 42 Color getColor(ubyte red, ubyte green, ubyte blue) { 43 return Color(red, green, blue); 44 } 45 46 /**Get a font in a GUI framework-agnostic way.*/ 47 Font getFont(string fontName, int size) { 48 return new Font(fontName, size); 49 } 50 51 /// 52 enum TextAlignment { 53 /// 54 Left = 0, 55 56 /// 57 Center = 1, 58 59 /// 60 Right = 2 61 } 62 63 // This calls the relevant lib's method of cleaning up the given object, if 64 // any. 65 void doneWith(T)(T garbage) { 66 static if(is(typeof(garbage.dispose()))) { 67 garbage.dispose(); 68 } 69 70 static if(is(T : Brush) || is(T : Pen)) { 71 // Having too many brushes and pens active seems to wreak havok on DFL. 72 delete garbage; 73 } 74 } 75 76 /**The DFL-specific parts of the Figure class. These include wrappers around 77 * the subset of drawing functionality used by Plot2Kill. 78 */ 79 class FigureBase : GuiAgnosticBase { 80 private: 81 // This is indexed via a TextAlignment enum. 82 TextFormat[3] textAlignments; 83 84 Rect roundedRect(double x, double y, double width, double height) { 85 // This code is designed to make sure the right/bottom of the rectangle 86 // always ends up as close as possible to where it was intended to, 87 // even if rounding fscks up the x/y coordinate, and to favor making 88 // the rectangle too big instead of too small since too big looks 89 // less bad. 90 91 // Round down to make rectangle wider than it should be since overshoot 92 // is less severe aesthetically than undershoot. 93 immutable intX = to!int(x); 94 immutable intY = to!int(y); 95 96 immutable endX = x + width; 97 immutable endY = y + height; 98 99 // Since overshoot looks better than undershoot, we want the minimum 100 // value of width and height that will not give undershoot. 101 immutable intWidth = to!int(ceil(endX - intX)); 102 immutable intHeight = to!int(ceil(endY - intY)); 103 104 return Rect(intX, intY, intWidth, intHeight); 105 } 106 107 static Font getRotated(Font font) { 108 LogFont lf; 109 110 lf.faceName = font.name; 111 112 lf.lf.lfEscapement = 900; 113 lf.lf.lfOrientation = 900; 114 115 lf.lf.lfOutPrecision = OUT_DEFAULT_PRECIS; 116 lf.lf.lfClipPrecision = CLIP_DEFAULT_PRECIS; 117 lf.lf.lfPitchAndFamily = DEFAULT_PITCH | FF_DONTCARE; 118 119 return new Font(lf, font.size, font.style, font.unit); 120 } 121 122 protected: 123 // Fonts tend to be different actual sizes on different GUI libs for a 124 // given nominal size. This adjusts for that factor when setting default 125 // fonts. 126 enum fontSizeAdjust = -3; 127 128 Graphics context; 129 130 this() { 131 textAlignments[TextAlignment.Left] = new TextFormat; 132 textAlignments[TextAlignment.Left].alignment 133 = dfl.drawing.TextAlignment.LEFT; 134 135 textAlignments[TextAlignment.Center] = new TextFormat; 136 textAlignments[TextAlignment.Center].alignment 137 = dfl.drawing.TextAlignment.CENTER; 138 139 textAlignments[TextAlignment.Right] = new TextFormat; 140 textAlignments[TextAlignment.Right].alignment 141 = dfl.drawing.TextAlignment.RIGHT; 142 } 143 144 145 public: 146 147 // Wrappers around DFL's drawing functionality. Other ports should have 148 // wrappers with the same compile-time interface. These are final both 149 // to avoid virtual call overhead and because overriding them would be 150 // rather silly. 151 final void drawLine 152 (Pen pen, double startX, double startY, double endX, double endY) { 153 context.drawLine(pen, 154 roundTo!int(startX + xOffset), roundTo!int(startY + yOffset), 155 roundTo!int(endX + xOffset), roundTo!int(endY + yOffset) 156 ); 157 } 158 159 final void drawLine(Pen pen, PlotPoint start, PlotPoint end) { 160 this.drawLine(pen, start.x, start.y, end.x, end.y); 161 } 162 163 final void drawRectangle 164 (Pen pen, double x, double y, double width, double height) { 165 auto r = roundedRect(x + xOffset, y + yOffset, width, height); 166 context.drawRectangle 167 (pen, r.x , r.y , r.width, r.height); 168 } 169 170 final void drawRectangle(Pen pen, PlotRect r) { 171 this.drawRectangle(pen, r.x, r.y, r.width, r.height); 172 } 173 174 final void fillRectangle 175 (Brush brush, double x, double y, double width, double height) { 176 auto r = roundedRect(x + xOffset, y + yOffset, width, height); 177 context.fillRectangle 178 (brush, r.x, r.y, r.width, r.height); 179 } 180 181 final void fillRectangle(Brush brush, PlotRect r) { 182 this.fillRectangle(brush, r.x, r.y, r.width, r.height); 183 } 184 185 final void drawText( 186 string text, 187 Font font, 188 Color pointColor, 189 PlotRect rect, 190 TextAlignment alignment 191 ) { 192 auto offsetRect = Rect( 193 roundTo!int(rect.x + xOffset), 194 roundTo!int(rect.y + yOffset), 195 roundTo!int(rect.width), 196 roundTo!int(rect.height) 197 ); 198 context.drawText( 199 text, font, pointColor, offsetRect, textAlignments[alignment]); 200 } 201 202 final void drawRotatedText( 203 string text, 204 Font font, 205 Color pointColor, 206 PlotRect rect, 207 TextAlignment alignment 208 ) { 209 auto rfont = getRotated(font); 210 scope(exit) doneWith(rfont); 211 212 auto meas = measureText(text, font); 213 auto slack = max(0, rect.height - meas.width); 214 double toAdd; 215 if(alignment == TextAlignment.Center) { 216 toAdd = slack / 2; 217 } else if(alignment == TextAlignment.Right) { 218 toAdd = slack; 219 } 220 221 // The rotated text patch is buggy, and will try to wrap words 222 // when it shouldn't. Make width huge to effectively disable 223 // wrapping. Also, DFL's Y coord is for the bottom, not the top. 224 // Fix this. 225 auto rect2 = PlotRect( 226 rect.x, rect.y + meas.width + toAdd, 227 10 * this.width, rect.width); 228 drawText(text, rfont, getColor(0, 0, 0), rect2); 229 } 230 231 final void drawText( 232 string text, 233 Font font, 234 Color pointColor, 235 PlotRect rect 236 ) { 237 auto offsetRect = Rect( 238 roundTo!int(rect.x + xOffset), 239 roundTo!int(rect.y + yOffset), 240 roundTo!int(rect.width), roundTo!int(rect.height) 241 ); 242 context.drawText(text, font, pointColor, offsetRect); 243 } 244 245 final Size measureText 246 (string text, Font font, double maxWidth, TextAlignment alignment) { 247 return context.measureText(text, font, roundTo!int(maxWidth), 248 textAlignments[alignment]); 249 } 250 251 final Size measureText 252 (string text, Font font, TextAlignment alignment) { 253 return 254 context.measureText(text, font, textAlignments[alignment]); 255 } 256 257 final Size measureText(string text, Font font, double maxWidth) { 258 return context.measureText(text, font, roundTo!int(maxWidth)); 259 } 260 261 final Size measureText(string text, Font font) { 262 return context.measureText(text, font); 263 } 264 265 // TODO: Add support for stuff other than solid brushes. 266 267 /**Get a brush in a GUI framework-agnostic way.*/ 268 final Brush getBrush(Color color) { 269 return new SolidBrush(color); 270 } 271 272 /**Get a pen in a GUI framework-agnostic way.*/ 273 final Pen getPen(Color color, double width = 1) { 274 return new Pen(color, max(roundTo!int(width), 1)); 275 } 276 277 void drawTo(Graphics context) { 278 drawTo(context, this.width, this.height); 279 } 280 281 // Weird function overloading bugs. This should be removed. 282 void drawTo(Graphics context, double width, double height) { 283 return drawTo(context, PlotRect(0, 0, width, height)); 284 } 285 286 // Allows drawing at an offset from the origin. 287 void drawTo(Graphics context, PlotRect whereToDraw) { 288 enforceSane(whereToDraw); 289 // Save the default class-level values, make the values passed in the 290 // class-level values, call draw(), then restore the default values. 291 auto oldContext = this.context; 292 auto oldWidth = this._width; 293 auto oldHeight = this._height; 294 auto oldXoffset = this.xOffset; 295 auto oldYoffset = this.yOffset; 296 297 scope(exit) { 298 this.context = oldContext; 299 this._height = oldHeight; 300 this._width = oldWidth; 301 this.xOffset = oldXoffset; 302 this.yOffset = oldYoffset; 303 } 304 305 this.context = context; 306 this._width = whereToDraw.width; 307 this._height = whereToDraw.height; 308 this.xOffset = whereToDraw.x; 309 this.yOffset = whereToDraw.y; 310 drawImpl(); 311 } 312 313 /**Saves this figure to a file. The file type can be one of the 314 * raster formats .png or .bmp. Saving to vector formats will likely 315 * never be supported on DFL because DFL's drawing backend is GDI, which is 316 * inherently raster-based. The width and height parameters allow you to 317 * specify explicit width and height parameters for the image file. If 318 * width and height are left at their default values 319 * of 0, the default width and height of the subclass being saved will 320 * be used. 321 * 322 * Note: The width and height parameters are provided as doubles for 323 * consistency with backends that support vector formats. These 324 * are simply rounded to the nearest integer for the DFL backend. 325 */ 326 void saveToFile 327 (string filename, string type, double width = 0, double height = 0) { 328 // User friendliness: Remove . if it was included, don't be case sens. 329 type = toLower(type); 330 if(!type.empty && type.front == '.') { 331 type.popFront(); 332 } 333 334 // Check this stuff upfront before we allocate a bunch of resources. 335 enforce(type == "bmp" || type == "png", 336 "Don't now how to save a " ~ type ~ " file on the DFL backend."); 337 338 enforce(width >= 0 && height >= 0, 339 "Can't save an image w/ negative or NaN width or height."); 340 341 if(width == 0 || height == 0) { 342 width = this.defaultWindowWidth; 343 height = this.defaultWindowHeight; 344 } 345 346 immutable iWidth = roundTo!int(width); 347 immutable iHeight = roundTo!int(height); 348 349 auto graphics = new MemoryGraphics(iWidth, iHeight); 350 scope(exit) doneWith(graphics); 351 352 this.drawTo(graphics, iWidth, iHeight); 353 File handle = File(filename, "wb"); 354 scope(exit) handle.close(); 355 356 auto pix = getPixels(graphics); 357 scope(exit) free(cast(void*) pix.ptr); 358 359 if(type == "bmp") { 360 writeBitmap(pix, handle, iWidth, iHeight); 361 } else if(type == "png") { 362 writePngFromBitmapPixels(pix, handle, iWidth, iHeight); 363 } else { 364 assert(0); // Already validated input at beginning of function. 365 } 366 } 367 368 /**Convenience function that infers the type from the filename extenstion 369 * and defaults to .png if no valid file format extension is found. 370 */ 371 void saveToFile(string filename, double width = 0, double height = 0) { 372 auto type = toLower(extensionNoDot(filename)); 373 374 if(type != "bmp") { 375 // Default to png. 376 type = "png"; 377 } 378 saveToFile(filename, type, width, height); 379 } 380 381 /// 382 FigureControl toControl() { 383 return new FigureControl(this); 384 } 385 386 /// 387 void showAsMain() { 388 Application.run(new DefaultPlotWindow(this.toControl)); 389 } 390 } 391 392 // Used for scatter plots. Efficiently draws a single character in a lot of 393 // places, centered on a point. ASSUMPTION: No drawing commands not related 394 // to drawing scatter plot points are issued between when initialize() 395 // and reset() are called. 396 package struct ScatterCharDrawer { 397 private: 398 string str; 399 PlotSize measurements; 400 Figure fig; 401 Font font; 402 Color color; 403 404 public: 405 this(dchar c, Font font, Color color, Figure fig) { 406 str = to!string(c); 407 this.fig = fig; 408 this.font = font; 409 this.color = color; 410 411 auto measInt = fig.measureText(str, font); 412 measurements = PlotSize(measInt.width, measInt.height); 413 } 414 415 void draw(PlotPoint where) { 416 if(!fig.insideAxes(where)) return; 417 fig.drawText(str, font, color, 418 PlotRect(where.x - measurements.width * 0.5, 419 where.y - measurements.height * 0.5, 420 measurements.width, 421 measurements.height 422 ) 423 ); 424 } 425 426 // Dummy function, but other GUI libs require something here. 427 void initialize() {} 428 429 // Dummy function, but other GUI libs require something here. 430 void restore() {} 431 } 432 433 // Fudge factors for the space that window borders take up. 434 // 435 // TODO: 436 // Figure out how to get the actual numbers and use them instead of these 437 // stupid fudge factors, though I couldn't find any API in DFL to do that. 438 private enum verticalBorderSize = 38; 439 private enum horizontalBorderSize = 16; 440 441 class FigureControl : PictureBox { 442 private: 443 FigureBase _figure; 444 445 package: 446 this(FigureBase fig) { 447 this._figure = fig; 448 this.size = Size(fig.minWindowWidth, fig.minWindowHeight); 449 } 450 451 void parentResize(Control c, EventArgs ea) { 452 453 454 immutable pwid = parent.width - horizontalBorderSize; 455 immutable pheight = parent.height - verticalBorderSize; 456 457 this.size = Size(parent.width - horizontalBorderSize, 458 parent.height - verticalBorderSize); 459 460 draw(); 461 } 462 463 public: 464 /// Event handler to redraw the figure. 465 void drawFigureEvent(Control c, EventArgs ea) { 466 draw(); 467 } 468 469 /**Get the underlying FigureBase object.*/ 470 final FigureBase figure() @property { 471 return _figure; 472 } 473 474 /// 475 void draw() { 476 this.setBounds(0, 0, this.width, this.height); 477 auto context = new MemoryGraphics(this.width, this.height); 478 479 figure.drawTo(context, this.width, this.height); 480 auto bmp = context.toBitmap; 481 this.image = bmp; 482 } 483 } 484 485 /// 486 class DefaultPlotWindow : Form { 487 private: 488 FigureControl control; 489 enum string fileFilter = "PNG files (*.png)|*.png|BMP files (*.bmp)|*.bmp"; 490 491 // Why does the save dialog use 1 indexing instead of zero indexing? 492 static immutable string[3] types = 493 ["dummy", "png", "bmp"]; 494 495 void errDialog(string eString) { 496 msgBox("File could not be saved successfully. " ~ eString); 497 498 } 499 500 // Brings up a save menu when the window is right clicked on. 501 void rightClickSave(Control c, MouseEventArgs ea) { 502 if(ea.button != MouseButtons.RIGHT) { 503 return; 504 } 505 506 auto dialog = new SaveFileDialog; 507 dialog.overwritePrompt = true; 508 dialog.filter = fileFilter; 509 dialog.validateNames = true; 510 dialog.showDialog(this); 511 512 // For now the only choice is bmp. Eventually we hope to support png. 513 if(dialog.fileName.length == 0) { 514 // User hit cancel. 515 return; 516 } 517 518 auto filename = dialog.fileName; 519 auto ext = toLower(extensionNoDot(filename)); 520 string type; 521 if(ext == "png" || ext == "bmp") { 522 type = ext; 523 } else { 524 type = types[dialog.filterIndex]; 525 filename ~= '.'; 526 filename ~= type; 527 } 528 529 try { 530 control.figure.saveToFile( 531 filename, 532 control.width, 533 control.height 534 ); 535 } catch(Exception e) { 536 errDialog(e.toString()); 537 } 538 } 539 540 public: 541 /// 542 this(FigureControl control) { 543 control.dock = DockStyle.FILL; 544 this.control = control; 545 control.size = Size( 546 control.figure.defaultWindowWidth, 547 control.figure.defaultWindowHeight 548 ); 549 550 this.size = Size( 551 control.width + horizontalBorderSize, 552 control.height + verticalBorderSize 553 ); 554 this.minimumSize = 555 Size(control.figure.minWindowWidth + horizontalBorderSize, 556 control.figure.minWindowHeight + verticalBorderSize); 557 558 this.resize ~= &control.parentResize; 559 this.activated ~= &control.drawFigureEvent; 560 control.mouseDown ~= &rightClickSave; 561 control.parent = this; 562 control.bringToFront(); 563 } 564 } 565 566 private: 567 // This stuff is an attempt at providing support for saving DFL plots to 568 // bitmaps. It borrows from Tomasz Stachowiak's excellent DirectBitmap code, 569 // which was licensed under the also excellent WTFPL. 570 571 import dfl.internal.winapi; 572 import std.c.stdlib : malloc, free; 573 574 // Get the bitmap as an array of pixels. Returns on the C heap b/c it's a 575 // private function that allodates huge buffers with trivial lifetimes, so 576 // we want to free them immediately. 577 Pixel[] getPixels(MemoryGraphics graphics) { 578 // Calculate bitmap padding. Bitmaps require the number of bytes per line 579 // to be divisible by 4. 580 int paddingBytes; 581 while((paddingBytes + graphics.width * 3) % 4 > 0) { 582 paddingBytes++; 583 } 584 585 immutable len = graphics.height * (graphics.width * 3 + paddingBytes); 586 auto pixels = (cast(byte*) malloc(len))[0..len]; 587 588 BITMAPINFO bitmapInfo; 589 with (bitmapInfo.bmiHeader) { 590 biSize = bitmapInfo.bmiHeader.sizeof; 591 biWidth = graphics.width; 592 biHeight = graphics.height; 593 biPlanes = 1; 594 biBitCount = 24; 595 biCompression = BI_RGB; 596 } 597 598 int result = GetDIBits( 599 graphics.handle, 600 graphics.hbitmap, 601 0, 602 graphics.height, 603 pixels.ptr, 604 &bitmapInfo, 605 DIB_RGB_COLORS 606 ); 607 608 enforce(result == graphics.height, "Reading bitmap pixels failed."); 609 610 if(paddingBytes > 0) { 611 // Remove padding bits. 612 size_t toIndex, fromIndex; 613 foreach(row; 0..graphics.height) { 614 foreach(i; 0..graphics.width * 3) { 615 pixels[toIndex++] = pixels[fromIndex++]; 616 } 617 618 fromIndex += paddingBytes; 619 } 620 } 621 622 return (cast(Pixel*) pixels.ptr)[0..graphics.width * graphics.height]; 623 } 624 625 extern(Windows) int GetDIBits( 626 HDC hdc, // handle to DC 627 HBITMAP hbmp, // handle to bitmap 628 UINT uStartScan, // first scan line to set 629 UINT cScanLines, // number of scan lines to copy 630 LPVOID lpvBits, // array for bitmap bits 631 LPBITMAPINFO lpbi, // bitmap data buffer 632 UINT uUsage // RGB or palette index 633 ); 634 635 636 enum UINT DIB_RGB_COLORS = 0; 637 enum UINT BI_RGB = 0; 638 639 struct Pixel { 640 align(1) { 641 ubyte b, g, r; 642 } 643 } 644 static assert (Pixel.sizeof == 3); 645 646 647 // Bugs: Doesn't work at all. I have no idea why. 648 version(none) { 649 /* This class contains an implementation of an ad-hoc message passing 650 * system that allows plots to be thrown up from any thread w/o 651 * an explicit main GUI window. This is useful for apps that are 652 * basically console apps except for the plots they display. 653 */ 654 private class ImplicitMain { 655 656 __gshared static { 657 Form implicitMain; 658 Button implicitButton; 659 Form[] toShow; 660 661 void initialize() { 662 synchronized(ImplicitMain.classinfo) { 663 if(implicitMain !is null) { 664 return; 665 } 666 667 implicitMain = new Form; 668 // implicitMain.visible = false; 669 670 implicitButton = new Button; 671 implicitButton.click ~= toDelegate(&showToShow); 672 implicitButton.parent = implicitMain; 673 } 674 675 static void doIt() { 676 auto ac = new ApplicationContext; 677 ac.mainForm = implicitMain; 678 Application.run(ac); 679 } 680 681 auto t = new Thread(&doIt); 682 t.isDaemon = true; 683 t.start; 684 doIt; 685 } 686 687 void showToShow(Control c, EventArgs ea) { 688 synchronized(ImplicitMain.classinfo) { 689 while(!toShow.empty) { 690 toShow.back().show(); 691 toShow.popBack(); 692 } 693 } 694 } 695 696 void addForm(Form form) { 697 synchronized(ImplicitMain.classinfo) { 698 toShow ~= form; 699 implicitButton.performClick(); 700 } 701 } 702 } 703 } 704 } 705 706 707 }