{"id":1270,"date":"2012-11-09T19:28:52","date_gmt":"2012-11-09T07:28:52","guid":{"rendered":"https:\/\/www.deltics.co.nz\/blog\/?p=1270"},"modified":"2012-11-09T19:28:52","modified_gmt":"2012-11-09T07:28:52","slug":"text-metrics-font-styles-tcanvas-and-device-contexts","status":"publish","type":"post","link":"https:\/\/www.deltics.co.nz\/blog\/posts\/1270\/","title":{"rendered":"Text Metrics, Font Styles, TCanvas and Device Contexts"},"content":{"rendered":"<span class=\"span-reading-time rt-reading-time\" style=\"display: block;\"><span class=\"rt-label rt-prefix\">[Estimated Reading Time: <\/span> <span class=\"rt-time\"> 5<\/span> <span class=\"rt-label rt-postfix\">minutes]<\/span><\/span><p>In putting some finishing polish on the GUI console of my <strong>Smoketest<\/strong> framework (see, I <em>am<\/em> working on it!) I ran into something that I remember I once knew and &#8211; seemingly &#8211; had forgotten, about mixing API level access to a GDI device context with the high-level <strong>TCanvas<\/strong> access that is conveniently provided for us.<br \/>\n<!--more--><br \/>\nFor a lot of basic drawing work, <strong>TCanvas<\/strong> is plenty good enough, but there are certain things that you cannot do with it, at least not directly.<\/p>\n<p>One of these is &#8211; or rather, used to be :<\/p>\n<ul>\n<li><em>Calculating the width of some text when rendered into some<br \/>\n     rectangle constrained to a <strong>maximum<\/strong> width, using ellipsis to fit if necessary.<\/em><\/li>\n<\/ul>\n<p>And other more complex text measurements other than just &#8220;how wide&#8221; or &#8220;how high&#8221; is this line of text.<\/p>\n<p>I say &#8220;<em>used to be<\/em>&#8220;, because you <em>can<\/em> now achieve this using <strong>TCanvas<\/strong>, as of at least Delphi 2010 (possibly <em>much<\/em> earlier &#8211; I haven&#8217;t checked), courtesy of additional overloads of the <strong>TextRect()<\/strong> method.  But previously to perform this calculation required that a call be made to the Windows GDI <strong>DrawTextEx()<\/strong> function.  Since <em>Smoketest<\/em> aims to remain compatible &#8211; at the core framework implementation level &#8211; with <strong>Delphi 7<\/strong>, this rules out the use of the new overload and demands the use of the Windows API.<\/p>\n<p>In any case, the point of this post is about the general problem of mixing certain GDI and <strong>TCanvas<\/strong> operations, not about solving that particular, specific problem.  I only use it as an example.<\/p>\n<p>So, first of all, let&#8217;s see the symptoms of the problem.<\/p>\n<p>The issue arose in my case with a simple calculation of the width of a label which was then used to determine the left edge position of a following piece of text.  This is most clearly seen in the &#8220;Inspections&#8221; output of the self-test suite:<\/p>\n<p><a href=\"https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Screen-Shot-2012-11-09-at-19.20.28-.png?ssl=1\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Screen-Shot-2012-11-09-at-19.20.28-.png?resize=402%2C229&#038;ssl=1\" alt=\"\" title=\"Inconsistent spacing between label and text\" width=\"402\" height=\"229\" class=\"aligncenter size-full wp-image-1271\" srcset=\"https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Screen-Shot-2012-11-09-at-19.20.28-.png?w=402&amp;ssl=1 402w, https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Screen-Shot-2012-11-09-at-19.20.28-.png?resize=300%2C170&amp;ssl=1 300w, https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Screen-Shot-2012-11-09-at-19.20.28-.png?resize=150%2C85&amp;ssl=1 150w\" sizes=\"auto, (max-width: 402px) 100vw, 402px\" \/><\/a><\/p>\n<p>The problem is the inconsistent spacing between the labels and the following value.  This spacing should be consistent and indeed, in the code, it is a simple absolute pixel adjustment applied to the <strong>TRect<\/strong> subsequently to be used for text value:<\/p>\n<pre class=\"brush: delphi; title: ; notranslate\" title=\"\">\r\n    aCanvas.Font.Style := &#x5B;fsBold];\r\n    DrawTextEx(dc, PChar(LabelText), Length(LabelText), LabelRect,\r\n               DT_CALCRECT or DT_WORD_ELLIPSIS or DT_NOPREFIX, NIL);\r\n    TextRect.Left := LabelRect.Right + 4;\r\n<\/pre>\n<p>Do you see the problem ?  Don&#8217;t worry, it isn&#8217;t immediately obvious.<\/p>\n<p>The <strong>dc<\/strong> parameter passed to <strong>DrawTextEx()<\/strong> is the <strong>HDC<\/strong> (device context handle) of the canvas we are performing the calculations for.  Since there are a large number of calculations being performed in this canvas, this handle is obtained and stored in a local variable for re-use, to avoid having to repeatedly use the <em>aCanvas.Handle<\/em> accessor. <\/p>\n<pre class=\"brush: delphi; title: ; notranslate\" title=\"\">\r\n  var\r\n    dc: HDC;\r\n  begin\r\n    :\r\n    dc := aCanvas.Handle;\r\n    :\r\n  end;\r\n<\/pre>\n<p>This however is the key to the problem.  Or at least a partner in the crime.  The accomplice is the fact that the label text we are calculating the width of is rendered in bold, requiring a change in the font properties.<\/p>\n<p>If we look at the implementation of <strong>TCanvas<\/strong> we find that this installed it&#8217;s own listener (handler if you prefer) on the <strong>OnChanged<\/strong> event of it&#8217;s <strong>Font<\/strong> object.  Changing the properties of the <strong>Font<\/strong> results in the following:<\/p>\n<pre class=\"brush: delphi; title: ; notranslate\" title=\"\">\r\nprocedure TCanvas.FontChanged(AFont: TObject);\r\nbegin\r\n  if csFontValid in State then\r\n  begin\r\n    Exclude(State, csFontValid);\r\n    SelectObject(FHandle, StockFont);\r\n  end;\r\nend;\r\n<\/pre>\n<p>Because the <strong>Font<\/strong> has changed, the canvas removes the <strong>csFontValid<\/strong> flag from it&#8217;s internal state.  But it also selects the <strong>StockFont<\/strong> into the device context.  i.e. having changed the properties of the <strong>Font<\/strong>, the device context is now using a completely different Font (unless the new font properties happen by coincidence to reflect those of <strong>StockFont<\/strong> itself of course)!<\/p>\n<p>If we were only using <strong>TCanvas<\/strong> methods, this would not be a problem.<\/p>\n<p>When we next access the <strong>TCanvas.Handle<\/strong> property, the canvas checks the internal state to make sure that everything is good to go.  If not, it takes whatever steps are necessary to rectify that.<\/p>\n<p>In the case of the <strong>Font<\/strong> properties having changed, the lack of <strong>csFontValid<\/strong> in the state will cause the canvas to create the required GDI font object and select it into the device context for us.<\/p>\n<p>But because I am specifically avoiding using that accessor, the device context is left with <strong>StockFont<\/strong> selected, which is absolutely not the font I expected, and in this case has quite different characteristics.  Which mean that when I then start measuring text, I get results that are not correct for the font I will subsequently use to actually render the text (the actual drawing code is able to use <strong>TCanvas<\/strong>, without having to mix-in GDI calls, so the problem only arises in calculations for the layout which that drawing code then relies on).<\/p>\n<p>The solution therefore is simply to ensure that after changing the <strong>Font<\/strong> properties, that I &#8220;touch&#8221; the <strong>TCanvas<\/strong> Handle property in order to ensure that the internal state of the device context is brought up-to-update:<\/p>\n<pre class=\"brush: delphi; title: ; notranslate\" title=\"\">\r\n    aCanvas.Font.Style := &#x5B;fsBold];\r\n    dc := aCanvas.Handle;  \/\/ Cause the changed font to be selected into the canvas\r\n     :\r\n    \/\/ Proceed to use dc ...\r\n<\/pre>\n<p>With all of the relevant areas of my layout code addressed, I now get the result I was aiming for.  Nice, consistent spacing:<\/p>\n<p><a href=\"https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Screen-Shot-2012-11-09-at-19.22.30-.png?ssl=1\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Screen-Shot-2012-11-09-at-19.22.30-.png?resize=406%2C225&#038;ssl=1\" alt=\"\" title=\"Label spacing consistent as expected and required\" width=\"406\" height=\"225\" class=\"aligncenter size-full wp-image-1278\" srcset=\"https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Screen-Shot-2012-11-09-at-19.22.30-.png?w=406&amp;ssl=1 406w, https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Screen-Shot-2012-11-09-at-19.22.30-.png?resize=300%2C166&amp;ssl=1 300w, https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Screen-Shot-2012-11-09-at-19.22.30-.png?resize=150%2C83&amp;ssl=1 150w\" sizes=\"auto, (max-width: 406px) 100vw, 406px\" \/><\/a><\/p>\n<p>It&#8217;s worth noting that the problem isn&#8217;t that our cached HDC is no longer &#8220;valid&#8221;.  The value of <strong>Handle<\/strong> (<strong>dc<\/strong>) itself doesn&#8217;t actually change since the device context doesn&#8217;t need to be re-created, we simply need the <em>side-effects<\/em> that come from simply accessing the handle.<\/p>\n<p>Also worth noting is that this behaviour is also seen with the other GDI objects exposed through wrapper objects on the canvas &#8211; the <strong>Pen<\/strong> and the <strong>Brush<\/strong>.<\/p>\n<p>The final thing to mention is that the tempting looking <strong>TCanvas.Refresh()<\/strong> method is a bit of a red-herring, since it does the exact <em>opposite<\/em> of what you might expect &#8211; it DE-selects all the current <strong>Pen<\/strong>, <strong>Brush<\/strong> and <strong>Font<\/strong> objects and selects the GDI <strong>StockPen<\/strong>, <strong>StockBrush<\/strong> and <strong>StockFont<\/strong>, marking all these objects as invalid so that they ALL get reselected when Handle is accessed.<\/p>\n<p><strong>Refresh()<\/strong> should perhaps really be called <strong>RefreshWhenINextAccessHandle()<\/strong>.<\/p>\n<h3>Footnote<\/h3>\n<p>There <em>is<\/em> a mechanism in <strong>TCanvas<\/strong> to enable you to explicitly request that a particular graphic object be selected into the device context &#8211; <strong>RequiredState()<\/strong>.  This is used all over the place in <strong>TCanvas<\/strong> itself to ensure that the objects required for any particular operation are up-to-date in the device context.<\/p>\n<p>Unfortunately this mechanism is a protected implementation detail, so if a well-behaved consumer of the class needs to trigger this mechanism, all they can do is rely on side-effects.  (When I say well-behaved, I mean if you don&#8217;t want to have to fiddle around with &#8220;cracker&#8221; classes or helpers etc to gain access to the protected members through a side-door)<\/p>\n","protected":false},"excerpt":{"rendered":"<p><span class=\"span-reading-time rt-reading-time\" style=\"display: block;\"><span class=\"rt-label rt-prefix\">[Estimated Reading Time: <\/span> <span class=\"rt-time\"> 5<\/span> <span class=\"rt-label rt-postfix\">minutes]<\/span><\/span>In putting some finishing polish on the GUI console of my Smoketest framework (see, I am working on it!) I ran into something that I remember I once knew and &#8211; seemingly &#8211; had forgotten, about mixing API level access to a GDI device context with the high-level TCanvas access that is conveniently provided for [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2},"jetpack_post_was_ever_published":false},"categories":[4],"tags":[292,190,189,191],"class_list":["post-1270","post","type-post","status-publish","format-standard","hentry","category-delphi","tag-delphi","tag-gdi","tag-tcanvas","tag-tfont"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_shortlink":"https:\/\/wp.me\/p1TKYv-ku","jetpack_sharing_enabled":true,"jetpack-related-posts":[],"_links":{"self":[{"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/posts\/1270","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/comments?post=1270"}],"version-history":[{"count":9,"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/posts\/1270\/revisions"}],"predecessor-version":[{"id":1281,"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/posts\/1270\/revisions\/1281"}],"wp:attachment":[{"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/media?parent=1270"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/categories?post=1270"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/tags?post=1270"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}