Coding in Delphi and Beyond - Thoughts from David Cornelius http://corneliusconcepts.tech/ en AI Saves the Day http://corneliusconcepts.tech/ai-saves-day <span property="dc:title">AI Saves the Day</span> <div class="node-taxonomy-container"> <h3 class="term-title"><i class="icon-hashtag theme-color"></i> Category</h3> <ul class="taxonomy-terms"> <li class="taxonomy-term"><a href="/miscellaneous" hreflang="en">Miscellaneous</a></li> </ul> </div> <!--/.node-taxonomy-container --> <span rel="sioc:has_creator"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span></span> <span property="dc:date dc:created" content="2024-10-27T23:20:05+00:00" datatype="xsd:dateTime">Sun, 10/27/2024 - 16:20</span> <div property="content:encoded" class="field field--name-body field--type-text-with-summary field--label-hidden field-item"><p><a href="/sites/default/files/inline-images/FastResearcher.png"><img alt="The AI Researcher" data-entity-type="file" data-entity-uuid="d05f2993-a0f6-47c6-a732-77dbdc0273b6" hspace="5" src="/sites/default/files/inline-images/FastResearcher.png" width="400" class="align-left" /></a>An online customer encountered a rare problem with a database which stumped me; AI came to the rescue.</p> <p>A friend of mine sells an insurance management system to agencies around the U.S. It's been a successful niche product for over 20 years, is written in Delphi 2007 so runs only on Windows, and uses the Firebird database on the back end. Until a few years ago, his customers all hosted their own local database server (or a designated user workstation) to hold the data. The application is very solid and chugs along with virtually no support calls related to corrupt databases.</p> <p>About six years ago, his customers were clamoring for an online solution so he asked me to help and we set up a couple of servers and started hosting his software and database in the cloud. One of the servers runs the Firebird database which easily handles the 70-90 users among a couple of dozen insurance offices.</p> <p>Recently we ran into a rare database problem. Every once in a great while, a PC will get disconnected from the database. Most of the time, the server recognizes it and cleans up the connection but in rare cases, it thinks the user is still connected. When this happens, the users is blocked and the only recourse in the past has been to restart the office database server. Each insurance agency has their own database but in the cloud, all the database requests go through one database server, so I didn't want to disrupt the other 80 or so users to get this one user back online.</p> <p>I knew there must be a fix but I don't use the Firebird database very often and encounter errors like this even less so wasn't sure how to rectify the situation. After mulling over several possible ways to handle this, each with multiple downsides, I decided to ask AI for a suggestion.</p> <p>Here's the short conversation I had with <a href="https://claude.ai/">Claude</a>:</p> <blockquote> <p><strong>DC</strong>: how do you tell firebird 2.15 to clear connections from a database?</p> <p><strong>AI</strong>: To clear connections from a Firebird 2.15 database, you can use the gfix utility with the -shut command. Here's how you can do it:</p> <p>1. Open a command prompt or terminal.<br /> 2. Use the gfix command with the following syntax:</p> <pre> <strong>gfix -user SYSDBA -password masterkey -shut -force 0 database_path</strong> </pre> <p>Replace <code>masterkey</code> with your SYSDBA password if it's different, and <code>database_path</code> with the full path to your Firebird database file.</p> <p>The <code>-shut</code> option shuts down the database, and <code>-force 0</code> disconnects all users immediately.</p> <p>3. After running this command, the database will be shut down and all connections will be cleared.<br /> 4. To bring the database back online, use:</p> <pre> <strong>gfix -user SYSDBA -password masterkey -online database_path</strong> </pre> <p>This method forcibly disconnects all users, so use it cautiously in a production environment. If you need a less disruptive approach or more specific control over which connections to clear, you might need to use Firebird's monitoring tables to identify specific connections and then use the API to terminate them individually.</p> <p>Would you like me to explain any part of this process in more detail?</p> </blockquote> <p>I was able to apply this fix quickly and it only affected this one insurance office, which was soon back to work.</p> <p>This is the main reason I'm loving AI and using it more and more: it can find things I've forgotten about very quickly. It also allows me to be a jack of many trades and look like a master of all!</p> </div> <section id="node-comment"> <article data-comment-user-id="0" id="comment-652" class="js-comment single-comment"> <header class="comment-user-picture"> <article typeof="schema:Person" about="/user/0"> </article> <mark class="hidden" data-comment-timestamp="1730128521"></mark> </header> <div class="single-comment-content-body"> <h3 class="single-comment-title"><a href="/comment/652#comment-652" class="permalink" rel="bookmark" hreflang="en">Only problem with AI …</a></h3> <div class="single-comment-meta"> <span><a rel="nofollow" href="https://blog.dummzeuch.de" lang="" typeof="schema:Person" property="schema:name" datatype="">Thomas Müller (not verified)</a> Mon, 10/28/2024 - 00:49</span> </div> <!-- /.single-comment-meta --> <div class="single-comment-content"> <div class="field field--name-comment-body field--type-text-long field--label-hidden field-item"><p>Only problem with AI (including Claude) is that it provides plausible sounding answers that are wrong or misleading. A few days ago I asked Claude for a Delphi implementation of Timsort. What I got looked plausible but it simply didn't work. It sorted about 10 entries correctly and but broke for more. The same happened with ChatGPT on other occasions with completely other topics. That makes me wary of anything those hyped "helpers" suggest.</p></div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=652&amp;1=default&amp;2=en&amp;3=" token="u3qiJnLeOMSK1GsJASIpSL3xMiLfYypfM-CIgq7GY1M"></drupal-render-placeholder> </div> </div> <!-- /.single-comment-content --> </article> <div class="indented"> <article data-comment-user-id="1" id="comment-653" class="js-comment single-comment"> <header class="comment-user-picture"> <article typeof="schema:Person" about="/user/1"> </article> <mark class="hidden" data-comment-timestamp="1730129778"></mark> </header> <div class="single-comment-content-body"> <h3 class="single-comment-title"><a href="/comment/653#comment-653" class="permalink" rel="bookmark" hreflang="en">Being wary is good.</a></h3> <div class="single-comment-meta"> <span><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span> Mon, 10/28/2024 - 08:36</span> <p class="visually-hidden">In reply to <a href="/comment/652#comment-652" class="permalink" rel="bookmark" hreflang="en">Only problem with AI …</a> by <a rel="nofollow" href="https://blog.dummzeuch.de" lang="" typeof="schema:Person" property="schema:name" datatype="">Thomas Müller (not verified)</a></p> </div> <!-- /.single-comment-meta --> <div class="single-comment-content"> <div class="field field--name-comment-body field--type-text-long field--label-hidden field-item"><p>I do not promote the idea that one should take a suggested AI answer and use it as a solution without verification. What I have found is that it either jump-starts ideas and thinking about how to solve a problem or, as in the case mentioned in this blog, reminds me of a tool I can use. I checked the documentation, verified the syntax, ran the command, and verified with the user the problem was fixed.</p> <p>AI is not doing my work for me, it's not going to replace my job as a professional programmer that understands user needs or application context or can test to see if a suggestion is a proper solution. It is, though, becoming a helpful research assistant.</p></div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=653&amp;1=default&amp;2=en&amp;3=" token="jkanhKOBmqyHflyFPm0tfebyZaxshOiyHfOOuOUa8sk"></drupal-render-placeholder> </div> </div> <!-- /.single-comment-content --> </article> </div> <div class="comment-form-wrap"> <h2 class="add-comment-title"><i class="icon-add_comment theme-color"></i> Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=190&amp;2=comment_node_blog&amp;3=comment_node_blog" token="e0jOZ2TNGIuKg0xziToRQOScMUXJ-_sb9vSuj4pviVI"></drupal-render-placeholder> </div> <!--/.comment-form --> </section> Sun, 27 Oct 2024 23:20:05 +0000 david 190 at http://corneliusconcepts.tech Introducing WebStencils http://corneliusconcepts.tech/introducing-webstencils <span property="dc:title">Introducing WebStencils</span> <div class="node-taxonomy-container"> <h3 class="term-title"><i class="icon-hashtag theme-color"></i> Category</h3> <ul class="taxonomy-terms"> <li class="taxonomy-term"><a href="/programming" hreflang="en">Programming</a></li> </ul> </div> <!--/.node-taxonomy-container --> <span rel="sioc:has_creator"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span></span> <span property="dc:date dc:created" content="2024-09-26T05:22:10+00:00" datatype="xsd:dateTime">Wed, 09/25/2024 - 22:22</span> <div property="content:encoded" class="field field--name-body field--type-text-with-summary field--label-hidden field-item"><p><img alt="web stencils" data-entity-type="file" data-entity-uuid="ce70f256-365d-46ab-875b-b6a2a82d1c1a" src="/sites/default/files/inline-images/WebStencils.png" width="300" class="align-right" />The latest version of RAD Studio, <a href="https://blogs.embarcadero.com/announcing-the-availability-of-rad-studio-12-2-athens/">version 12.2</a>, was recently released and among the <a href="https://blogs.embarcadero.com/rad-studio-12-2-every-new-and-enhanced-feature/">many enhancements</a> was the introduction of a new template engine called <a href="https://blogs.embarcadero.com/rad-studio-web-development-reimagined-with-the-webstencils-template-engine/">WebStencils</a>. It's the new generation of text processing for WebBroker and RAD Server applications that produce HTML on web servers. But it's more than that as it can be used in Delphi or C++Builder applications of all kinds to generate JSON, XML, CSV, or any other template-based text output.</p> <p>I was eager to learn this new method for Delphi-generated content as I had an immediate need to build a web server in Delphi. The best way I know how to learn a new technology is to compare it to something with which I already have experience. Since I've built WebBroker applications before, this was the natural place to start.</p> <h2>Building Two Simple WebBroker Programs</h2> <p>Therefore, my approach to learning WebStencils was to build two simple WebBroker apps, one using the old <a href="https://docwiki.embarcadero.com/Libraries/Athens/en/Web.HTTPProd.TPageProducer">TPageProducer</a> components and one using the new <a href="https://docwiki.embarcadero.com/RADStudio/Athens/en/WebStencils#WebStencils_Components">TWebStencilsProcessor</a> components. These two simple apps were designed to do exactly the same thing and look nearly identical. This way, I was able to compare how similar tasks were done with each of the technologies.</p> <p>The programs start with a login screen, present a list of customers, and take you to a customer edit screen by clicking on the ID of a customer in the list. The edit screen doesn't actually save anything and there are no complicated queries or fancy web frameworks. I just wanted to see how WebStencils works differently than PageProducers, so didn't want to take the time or muddy the code by writing a full-featured website for this learning exercise.</p> <p>The two projects are both VCL WebApps, meaning they run as simple Windows apps that prompt for a port on which to listen, provide a button to start listening on that port for web requests, and another to open your default web browser to that port. It's what you get when you start a new <strong>Web Server Application</strong> in Delphi and select <em>Stand-alone GUI application</em> for the WebBroker Project Type.</p> <p>The web module in each project has four <code>TWebActionItems</code> with the following <em>PathInfo</em> values:</p> <ul> <li><strong><code>/</code></strong> (method: GET) default path prompts for a login</li> <li><strong><code>/login</code></strong> (method: POST) authorizes the given username and password and if successful, redirects to <code>/custlist</code></li> <li><strong><code>/custlist</code></strong> (method: GET) lists some customers from a sample database</li> <li><strong><code>/custedit</code></strong> (method: GET) presents the fields of a selected customer on an edit page</li> </ul> <p>Both projects are on <strong><a href="https://github.com/corneliusdavid/webstencils-demo">Github</a></strong>; the README there explains more details of how to set up the database and build the projects if you want to try them out.</p> <h2>Comparing PageProducers and WebStencils</h2> <p>As a quick review, the <code>TPageProducer</code> components take either strings or a file as a template and produce textual output (usually HTML). They do that by parsing each line and replacing special HTML-style tags. There are half a dozen built-in tags it already knows about like <code>&lt;#Link&gt;</code>, <code>&lt;#Image&gt;</code>, <code>&lt;#Table&gt;</code>, and a few more; but you'll likely create several of your own custom ones and handle them in <code>OnHTMLTag</code> events. A common way to generate a table of rows and columns is to embed the results of a <code>TDataSetTableProducer</code> into the content result of a PageProducer.</p> <p>The <code>TWebStencilsProcessor</code> components work similarly: they take either strings or a file as a template and produce textual output (usually HTML). But that's where the similarities mostly end and the interesting differences start! Instead of looking for HTML tags and simply doing a textual replacement per tag, WebStencils uses an <code>@</code> symbol that delineates either scripting keywords, objects registered with the WebStencils engine, or (like the PageProducers) return simple textual replacements by using <code>OnGetValue</code> events. The scripting keywords can contain conditional expressions, import a file, or iterate over a data set to generate a table of rows and columns (without needing to use an additional component and providing much more flexibility).</p> <p>The concepts and keywords I'll cover in this article include:</p> <ul> <li><code>@Layout</code></li> <li><code>@RenderBody</code></li> <li><code>@Import</code></li> <li><code>@<em>object.property</em></code></li> <li><code>@LoginRequired</code></li> <li><code>@foreach { }</code></li> <li><code>@if { }</code></li> </ul> <p>I'll start by looking at the first two and explain the framework used in these demos.</p> <h2>Laying Out the Framework</h2> <p>I tried to keep all the HTML in separate files for easier management and comparison. The exception was specifying a common web framework (header and footer) using PageProducers. For this, I used embedded strings in two additional <code>TPageProducer</code> components (<em>ppPageHeader</em> and <em>ppPageFooter</em>) to contain the HTML parts for each. Therefore, each PageProducer's HTML content would look like this:</p> <pre> <strong>&lt;#Header&gt;</strong>   &lt;h2&gt;Content Title&lt;/h2&gt;   ... content ... <strong>&lt;#Footer&gt;</strong> </pre> <p>and the PageProducer's <code>OnHTMLTag</code> event would have to replace the <code>&lt;#header&gt;</code>, and <code>&lt;#footer&gt;</code>, custom tags with the top and bottom part of the HTML page:</p> <pre> procedure TwebCustListWebBroker.ppAllHTMLTags(...); begin   if SameText(TagString, 'Header') then     ReplaceText := ppPageHeader.Content   else if SameText(TagString, 'Footer') then     ReplaceText := ppPageFooter.Content; end; </pre> <p>Using WebStencils, I was able to fully separate out a framework file that can wrap around the content body (resulting in one more HTML file but two less components on the web module for the that project). This is accomplished by using the <code>@LayoutPage</code> keyword like this:</p> <pre> <strong>@LayoutPage FrameworkFilename.html</strong>   &lt;h2&gt;Content Title&lt;/h2&gt;   ... content ... </pre> <p>In this case, the entire framework for the website (header <em>and</em> footer) is contained in one file making it much easier to work with in an HTML editor because you don't have to manually paste the strings from the header and footer text in the two PageProducers. The only thing you need to do to make it a WebStencils framework page is place the WebStencils keyword, <code>@RenderBody</code> in where each page's content will go. Then, each generated page that references this framework, automatically gets it's content inserted into this framework file producing the resulting HTML page.</p> <p>Here's a stripped down version of the framework page I built for this demo:</p> <pre> &lt;!DOCTYPE html&gt; &lt;html lang="en"&gt; &lt;body&gt;   &lt;div class="container px-5"&gt;     <strong>@RenderBody</strong>   &lt;div&gt;&lt;em&gt;Customer List Demo&lt;/em&gt;&lt;/div&gt; &lt;/body&gt; &lt;/html&gt; </pre> <p>It's easy to switch the framework by simply giving the <code>@LayoutPage</code> a different filename. And an extra bonus for using WebStencils is there's no extra Delphi code needed to implement this.</p> <h2>Importing External Files</h2> <p>Web page headers can get very large, especially if styles are defined there. For this reason, styles are often listed in .CSS files and referenced in <code>&lt;script&gt;</code> tags of the HTML header. I'm going to add styles in a separate file as well but will let WebBroker include them directly into the generated HTML. Let's see how to include external files using these two text processing paradigms.</p> <p>There's nothing special to importing an external file using PageProducers, simply create another custom tag and add another conditional statement in the <code>OnHTMLTag</code> event to use the content from another PageProducer.</p> <p>Here's the HTML in the header text:</p> <pre> &lt;style&gt;   <strong>&lt;#IncludeCSS&gt;</strong> &lt;/style&gt; </pre> <p>And the Delphi code in the <code>OnHTMLTag</code> event is straight-forward, where <em>ppStyles</em> is a <code>TPageProducer</code> linked to a file named, "style.css":</p> <pre> procedure TwebCustListWebBroker.ppAllHTMLTags(...; var ReplaceText: string); begin if SameText(TagString, 'IncludeCSS') then ReplaceText := ppStyles.Content; end;</pre> <p>The WebStencils version, however, provides a magic keyord that, again, saves us a little code.</p> <p>Here's the HTML:</p> <pre> &lt;style&gt;   <strong>@Import style.css</strong> &lt;/style&gt; </pre> <p>No need for any Delphi code here, WebStencils knows how to include an external file and the parameter to the keyword specifies the filename.</p> <h2>Replacing Template Variables</h2> <p><img alt="PageProducer app and version" data-entity-type="file" data-entity-uuid="44e6e8ae-3a1c-4eb0-83dd-531552b9a40b" src="/sites/default/files/inline-images/WebHeaderPP.png" width="300" class="align-right" />At the top of each web page in these demos is listed the application name and the version. For the PageProducer versions, custom HTML tags are used and the HTML snippet for them looks like this:</p> <pre> &lt;h2&gt;<strong>&lt;#AppName&gt;</strong>&lt;/h2&gt; &lt;h4&gt;Version <strong>&lt;#AppVersion&gt;</strong>&lt;/h4&gt;</pre> <p>Here, the <code>&lt;#AppName&gt;</code> tag is replaced in the PageProducer's <code>OnHTMLTag</code> event with a constant defined in the Delphi app for the application name; same with the <code>&lt;#AppVersion&gt;</code> tag.</p> <pre> procedure TwebCustListWebBroker.ppHTMLTags(...); begin   if SameText(TagString, 'AppName') then     ReplaceText := APP_NAME   else if SameText(TagString, 'AppVersion') then     ReplaceText := APP_VERSION; end; </pre> <p><img alt="WebStencils app and version" data-entity-type="file" data-entity-uuid="113eed4c-9afa-4525-91e1-48e9e2dc853f" src="/sites/default/files/inline-images/WebHeaderWS.png" width="300" class="align-right" />In the WebStencils version, the framework file has something very similar:</p> <pre> &lt;h2&gt;<strong>@App.Title</strong>&lt;/h2&gt; &lt;h4&gt;Version <strong>@App.Version</strong>&lt;/h4&gt;</pre> <p>Instead of HTML tags, you'll see an <code>@</code> symbol with an object name and dotted property name that gets replaced. I could have used an <code>OnGetValue</code> event to mimic the technique used by the PageProducers; instead, I registered an object (the form) with the <code>TWebStencilsEngine</code> component on the web module and named it <em>App</em>, with <code>FTitle</code> and <code>FVersion</code> as private fields on the form but surfaced as public properties (without the "F") so that they're accessible by the WebStencilsEngine:</p> <pre> procedure TwebCustListWebStencil.WebModuleCreate(Sender: TObject); begin   FTitle := 'Customer List for WebStencils';   FVersion := '0.3';   wsEngineCustList.AddVar('App', Self, False); end; </pre> <p>Now, any time a WebStencils processor component tied to that engine encounters an object in the text named <code>@App</code>, it knows to look at that registered object and replace the text with the value of that named property--no need for an event handler!</p> <h2>Requiring Authorization</h2> <img alt="customer denied" data-entity-type="file" data-entity-uuid="d1d0c348-4b40-426b-9ea8-912a79156079" src="/sites/default/files/inline-images/CustListDeniedPP.png" width="500" class="align-center" /> <p>Before I show the customer list, I need to make sure the user is authorized to view it. In the PageProducers version, I created a Boolean property, <code>FIsLoggedIn</code> and initialized it to <code>False</code>. Then, the <code>/login</code> action sets it to <code>True</code> and redirects to the <code>/custlist</code> page if authenticated or displays the error message shown above if not:</p> <pre> procedure TwebCustListWebBroker.LoginVerifyAction(...); var   Username, Password: string; begin   Username := Request.ContentFields.Values['uname'];   Password := Request.ContentFields.Values['psw'];   if dmCust.LoginCheck(Username, Password) then   begin     <strong>FIsLoggedIn := True;</strong>     Response.SendRedirect('/custlist');   end else     Response.Content := ppLoginFailed.Content;   Handled := True; end; </pre> <p>Each page needs to check to see if the user is logged in. To do this with PageProducers, each web action needs to check <code>FIsLoggedIn</code> and either returns the requested content if authorized or shows the "Access Denied" page:</p> <pre> procedure TwebCustListWebBroker.ListCustomersAction(...); begin   if IsLoggedIn then     Response.Content := ppCustList.Content   else     Response.Content := ppAccessDenied.Content;   Handled := True; end; </pre> <p>Authentication is a common need for accessing information these days. Conveniently, WebStencils processors provide a way to prevent their content from showing unless the user is logged in. This is accomplished by including the following keyword at the top of the HTML:</p> <p><code>@LoginRequired</code></p> <p>In order to tell the processor that the user is authenticated and to go ahead and show the content, we need to set a property for each processor that might need to know:</p> <pre> procedure TwebCustListWebStencils.LoginVerifyAction(...); var   Username, Password: string; begin   Username := Request.ContentFields.Values['uname'];   Password := Request.ContentFields.Values['psw'];   if dmCust.LoginCheck(Username, Password) then   begin     <strong>wspCustList.UserLoggedIn := True;</strong>     <strong>wspCustEdit.UserLoggedIn := True;</strong>     Response.SendRedirect('/custlist');   end else     Response.Content := ppLoginFailed.Content;   Handled := True; end; </pre> <p>There is no other Delphi code required once those properties are set but if the user tries to access a page with the keyword listed and the processor does not think the user is logged in, the WebStencils engine will throw an exception that can be caught in the processor for the page on which authentication failed:</p> <pre> ... try   Response.Content := wspCustList.Content; except   <strong>on E:EWebStencilsLoginRequired do</strong>     Response.Content := wspAccessDenied.Content; end; ... </pre> <p>If you don't handle it yourself, an Internal Application Error will be displayed to the user:</p> <img alt="internal app error" data-entity-type="file" data-entity-uuid="78e36cf2-c808-4f3c-882b-55a4379a0dd0" src="/sites/default/files/inline-images/CustListDeniedWS.png" width="500" class="align-center" /> <h2>Iterating Over a Dataset</h2> <p>Now that I've got a framework in place with included styles and the user has been properly authenticated, it's time to show some data!</p> <p>The sample database for this application has a few customers that I'll list in an HTML table. I used an HTML table instead of a CSS table to be compatible with the PageProducers version of these projects.</p> <p>I showed the code for the <code>/custlist</code> action handler above (see the "ListCustomersAction" method); it simply returns the content of the <code>ppCustList</code> PageProducer, which is this HTML:</p> <pre> &lt;#header&gt;   &lt;h2&gt;Customers&lt;/h2&gt;   <strong>&lt;#customers&gt;</strong> &lt;#footer&gt; </pre> <p>The <code>OnHTMLTag</code> event returns the content of the <code>TDataSetTableProducer</code> in place of the <code>&lt;#customers&gt;</code> tag:</p> <pre> procedure TwebCustListWebBroker.ppAllHTMLTags(...); begin   if SameText(TagString, 'Customers') then     <strong>ReplaceText := pptblCustomers.Content;</strong> end; </pre> <p>The <code>TDataSetTableProducer</code> is convenient in that, given a dataset, it will open and close it for you, wrap each row of data in a <code>&lt;tr&gt;</code> tag and each field of data in a <code>&lt;td&gt;</code> tag, and allow some customization with its properties and a couple of event handlers for styling and custom processing (which I'll get to in a bit). But it is stuck in the early days of writing HTML.</p> <p>This is the part of WebStencils that begins to show the powerful scripting capabilities of this engine. Instead of tying our dataset to a component that only knows how to do one thing (build an HTML table), I'll use the <code>@foreach</code> keyword in WebStencils to iterate over the records of a dataset. I'll register the dataset with the WebStencilsEngine, open it before and close it after the content is generated but leave the format of each field and row to be completely defined in the HTML template.</p> <p>First, here's the WebActionHandler for <code>/custlist</code> (minus the exception block for user authentication I showed earlier):</p> <pre> procedure TwebCustListWebStencil.ListCustomersAction(...); begin   dmCust.qryCustomers.Open;   try     <strong>if not wsEngineCustList.HasVar('CustList') then</strong>       <strong>wsEngineCustList.AddVar('CustList', dmCust.qryCustomers, False);</strong>     <strong>Response.Content := wspCustList.Content;</strong>   finally     dmCust.qryCustomers.Close;   end; end; </pre> <p>First, I check to see if the object name we're giving for the dataset, <em>CustList</em>, is already registered by using the <code>HasVar</code> function; if not, I add it. Then I simply return the content of the WebSencils processor, which is the HTML template for the customer list:</p> <pre> @LayoutPage CustListFramework1.html @LoginRequired &lt;h2&gt;Customers&lt;/h2&gt; &lt;table CellSpacing=10 CellPadding=8 Border=1&gt;   &lt;tr&gt;     &lt;th Align="right"&gt;ID&lt;/th&gt;     &lt;th Align="left"&gt;First&lt;/th&gt;     &lt;th Align="left"&gt;Last&lt;/th&gt;     &lt;th Align="left"&gt;Company&lt;/th&gt;   &lt;/tr&gt;   <strong>@foreach CustList {</strong>     &lt;tr&gt;       &lt;td align="right" &gt;&lt;a href="\[email protected]"&gt;<strong>@loop.CustomerId</strong>&lt;/a&gt;&lt;/td&gt;       &lt;td&gt;<strong>@loop.FirstName</strong>&lt;/td&gt;       &lt;td&gt;<strong>@loop.LastName</strong>&lt;/td&gt;       &lt;td&gt;<strong>@loop.Company</strong>&lt;/td&gt;     &lt;/tr&gt;   <strong>}</strong> &lt;/table&gt; </pre> <p>The <code>@foreach</code> keyword iterates over each row of the registered dataset (<em>CustList</em>). The template for each table row is given within the curly braces; in our demo case, it starts and ends with a <code>&lt;tr&gt;</code> tag. The WebStencils syntax provides the <code>@loop</code> keyword which acts as a temporary object for each row, providing acces to the fields CustomerID, FirstName, LastName, and Company.</p> <p>Again, I used HTML tables only so the resulting web pages produced by these two apps would be the same. This list could just as easily have been generated with CSS or a JavaScript library.</p> <h2>Including Template Text Conditionally</h2> <p>There is a lot more to WebStencils but I'm only going to touch on one more keyword. By reviewing the output of the generated customer list, I noticed not all customers have a company name, so I made a small modification to the output to color the rows that have a company name to make them stand out.</p> <p>To do that, I added a calculated field to the dataset called <em>IsBusiness</em> that I can check while generating each row; then I added a style class in the HTML header called <em>company_row</em> to add to the <code>&lt;td&gt;</code> tags on rows that have a non-blank company name.</p> <p>With PageProducers, this is all done in Delphi code using the <code>OnFormatCell</code> event handler of the <code>TDatsSetTableProducer</code> where I set the <code>CustomAttrs</code> property of all fields (except CustomerID) based on whether the <code>IsBusiness</code> calculated field is True or not:</p> <pre> procedure TwebCustListWebBroker.pptblCustomersFormatCell(...);begin if CellRow &gt; 0 then begin if CellColumn = 0 then // CustomerID CellData := Format('&lt;a href="\custedit?cust_no=%s"&gt;%s&lt;/a&gt;', [CellData, CellData]) else <strong>if pptblCustomers.DataSet.FieldByName('IsBusiness').AsBoolean</strong> then <strong>CustomAttrs := 'class="company_row"'</strong>; end; end; </pre> <p>In the WebStencils version, the only thing that changes is the HTML template because I can use the <code>@if</code> keyword to provide conditional processing right within the template! The <code>@foreach</code> loop that generates each dataset row now looks like this:</p> <pre> @foreach CustList {   &lt;tr&gt;     &lt;td align="right" &gt;&lt;a href="\[email protected]"&gt;@loop.CustomerId&lt;/a&gt;&lt;/td&gt;     &lt;td <strong>@if loop.IsBusiness {</strong> class="company_row"<strong> }</strong>&gt;@loop.FirstName&lt;/td&gt;     &lt;td <strong>@if loop.IsBusiness {</strong> class="company_row" <strong>}</strong>&gt;@loop.LastName&lt;/td&gt;     &lt;td <strong>@if loop.IsBusiness {</strong> class="company_row"<strong> }</strong>&gt;@loop.Company&lt;/td&gt;   &lt;/tr&gt; } </pre> <p>Once again, curly braces are used to delineate the block of text that should be generated if the expression <code>loop.IsBusiness</code> evaluates to True for those rows.</p> <h2>Going Further</h2> <p>I only touched on a few things; there are more keywords and properties and events you can use. I also didn't explain the difference between the <code>TWebStencilsEngine</code> and the <code>TWebStencilsProcessor</code> components; I mainly wanted to show how everything worked; read the <a href="https://docwiki.embarcadero.com/RADStudio/Athens/en/WebStencils">WebStencils documentation</a> for details.</p> <p>Also, be sure and check out the recent Embarcadero blog, <a href="https://blogs.embarcadero.com/rad-studio-web-development-reimagined-with-the-webstencils-template-engine/">RAD Studio Web Development Reimagined with the WebStencils Template Engine</a>. It mentions there are samples and possibly a book coming to elaborate on this powerful new text-processing engine.</p> </div> <section id="node-comment"> <article data-comment-user-id="0" id="comment-654" class="js-comment single-comment"> <header class="comment-user-picture"> <article typeof="schema:Person" about="/user/0"> </article> <mark class="hidden" data-comment-timestamp="1730570836"></mark> </header> <div class="single-comment-content-body"> <h3 class="single-comment-title"><a href="/comment/654#comment-654" class="permalink" rel="bookmark" hreflang="en">Example failed (for me)</a></h3> <div class="single-comment-meta"> <span><span lang="" typeof="schema:Person" property="schema:name" datatype="">Joe Donth (not verified)</span> Sat, 11/02/2024 - 09:13</span> </div> <!-- /.single-comment-meta --> <div class="single-comment-content"> <div class="field field--name-comment-body field--type-text-long field--label-hidden field-item"><p>I copied your examples, copies the DB and when I try to run the WebBroker version I get:</p> <p>Project CustListWebBroker.exe raised exception class EFDException with message '[FireDAC][Stan][Def]-254. Definition [WebStencilsChinook] is not found in [C:\Users\Public\Documents\Embarcadero\Studio\FireDAC\FDConnectionDefs.ini]'.</p> <p>Thoughts?</p> <p> </p></div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=654&amp;1=default&amp;2=en&amp;3=" token="3WKGrSUOdS-_5_O4DarppH_hFQWNBZBOK_HzD752pVc"></drupal-render-placeholder> </div> </div> <!-- /.single-comment-content --> </article> <div class="indented"> <article data-comment-user-id="1" id="comment-656" class="js-comment single-comment"> <header class="comment-user-picture"> <article typeof="schema:Person" about="/user/1"> </article> <mark class="hidden" data-comment-timestamp="1730584613"></mark> </header> <div class="single-comment-content-body"> <h3 class="single-comment-title"><a href="/comment/656#comment-656" class="permalink" rel="bookmark" hreflang="en">Two ways to set up the database</a></h3> <div class="single-comment-meta"> <span><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span> Sat, 11/02/2024 - 14:56</span> <p class="visually-hidden">In reply to <a href="/comment/654#comment-654" class="permalink" rel="bookmark" hreflang="en">Example failed (for me)</a> by <span lang="" typeof="schema:Person" property="schema:name" datatype="">Joe Donth (not verified)</span></p> </div> <!-- /.single-comment-meta --> <div class="single-comment-content"> <div class="field field--name-comment-body field--type-text-long field--label-hidden field-item"><p>There are two ways to specify where the database file resides, 1) by creating a FDConnection Definition or 2) simply specifying all the database parameters directly in the FDConnection component itself.</p> <p>I chose the first option: Expand the FireDAC Providers in the Data Explorer pane in Delphi, and create a new connection definition for SQLite, I pointed it to the database file, and gave it a name. Then, in the FDConnection Editor of the TFDConnection component on the project's data module, I selected that saved definition in the "Connection Definition Name" drop-down list.</p> <p>What you can do instead is set the properties of the data module's TFDConnection component directly: select the SQLite Driver ID (leaving the Connection Definition Name box blank), point it to your database file, and optionally set any other parameters you might need.</p> <p>Either way, you have to specify the Driver ID and database filename, either in a saved connection in the Data Explorer, or directly in the TFDConnection component itself.</p></div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=656&amp;1=default&amp;2=en&amp;3=" token="GwaAe3PXPmGNfv4iylsxBHicp7MvOWFX2_V14ntGVnU"></drupal-render-placeholder> </div> </div> <!-- /.single-comment-content --> </article> </div> <div class="comment-form-wrap"> <h2 class="add-comment-title"><i class="icon-add_comment theme-color"></i> Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=189&amp;2=comment_node_blog&amp;3=comment_node_blog" token="Q0pNWCeWshkcv1ErWkPPmRd2p9rTZOuGhXIhcEkbuf8"></drupal-render-placeholder> </div> <!--/.comment-form --> </section> Thu, 26 Sep 2024 05:22:10 +0000 david 189 at http://corneliusconcepts.tech Data Connection Backups http://corneliusconcepts.tech/data-connection-backups <span property="dc:title">Data Connection Backups</span> <div class="node-taxonomy-container"> <h3 class="term-title"><i class="icon-hashtag theme-color"></i> Category</h3> <ul class="taxonomy-terms"> <li class="taxonomy-term"><a href="/programming" hreflang="en">Programming</a></li> </ul> </div> <!--/.node-taxonomy-container --> <span rel="sioc:has_creator"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span></span> <span property="dc:date dc:created" content="2024-09-20T15:24:26+00:00" datatype="xsd:dateTime">Fri, 09/20/2024 - 08:24</span> <div property="content:encoded" class="field field--name-body field--type-text-with-summary field--label-hidden field-item"><p>I haven't used Delphi's <a href="https://docwiki.embarcadero.com/RADStudio/Athens/en/Data_Explorer">Data Explorer</a> all that much in years past but in my current job, we maintain hundreds of small accounting programs for dozens of clients, each using a different database and most of which we have backups for testing and support. I had created FireDAC connection definitions for about a dozen of them so I could easily switch between them at design time and view tables and structures from within the IDE. I've found this very convenient and have grown fond of the feature.</p> <p>I've known that upgrading even minor versions of Delphi uninstalls GetIt packages (which is why I wrote <a href="https://github.com/corneliusdavid/AutoGetIt">AutoGetIt</a>) but did not know that any Data Explorer definitions would also be lost. After upgrading from 12.1 to 12.2 this morning, I loaded a project group I was working on yesterday and suddenly encountered several <code>Definition [xxx] is not found in [...FDConnectionDefs.ini]</code> error messages.</p> <p>At first, I panicked, thinking I would have to tediously recreate all those connection definitions. But I noticed the error message listed the file where it expected to find the definitions and figured I should look in that folder to see if there was any way to recover the ones I had established before the upgrade. </p> <p>Sure enough, I found a few .BAK files that were date/time stamped with past dates, one just a few days earlier. I made a copy of the most recent backup, deleted the new, nearly empty .INI file that was created during the Delphi upgrade, then renamed my copied backup to the default filename as shown in the error messages. When I restarted Delphi, all my database connection definitions were back!<br />  </p> </div> <section id="node-comment"> <div class="comment-form-wrap"> <h2 class="add-comment-title"><i class="icon-add_comment theme-color"></i> Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=188&amp;2=comment_node_blog&amp;3=comment_node_blog" token="2VdRs548VE63vl2RtPhAAFN3Q09xqNm47lWqIgWKiHY"></drupal-render-placeholder> </div> <!--/.comment-form --> </section> Fri, 20 Sep 2024 15:24:26 +0000 david 188 at http://corneliusconcepts.tech AI Image Generation http://corneliusconcepts.tech/ai-image-generation <span property="dc:title">AI Image Generation</span> <div class="node-taxonomy-container"> <h3 class="term-title"><i class="icon-hashtag theme-color"></i> Category</h3> <ul class="taxonomy-terms"> <li class="taxonomy-term"><a href="/miscellaneous" hreflang="en">Miscellaneous</a></li> </ul> </div> <!--/.node-taxonomy-container --> <span rel="sioc:has_creator"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span></span> <span property="dc:date dc:created" content="2024-09-06T06:20:56+00:00" datatype="xsd:dateTime">Thu, 09/05/2024 - 23:20</span> <div property="content:encoded" class="field field--name-body field--type-text-with-summary field--label-hidden field-item"><p><a href="/sites/default/files/inline-images/GreekRomanSoldierProgramming.png"><img alt="Roman Programmer" data-entity-type="file" data-entity-uuid="8489dbd5-aa8b-4571-8c96-1718143b6b12" height="247" hspace="10" src="/sites/default/files/inline-images/GreekRomanSoldierProgramming.png" width="247" class="align-left" /></a>The fascinating realm of AI image generation has opened a new playground intersecting art, human creativity, and serious back-end computing. It's also opened questions of how we will tell real from fiction as this technology evolves and becomes even more sophisticated than it is already. For now, I'm just having some fun in my spare time!</p> <p>From the vast store of uploaded images and video that <a href="https://photutorial.com/photos-statistics/">grow by the billions every day</a>, high-end computers using machine learning algorithms combined with large language models (LLMs), can turn a simple textual prompt into an image that is often quite realistic. For example, this image was generated with the prompt, "greek roman soldier wearing helmet and glasses sitting at a computer programming" and was generated using <a href="https://www.artspace.ai/?ref=delphidave">ArtSpace.ai</a>. (I'm pretty sure he's a Delphi programmer, don't you?)</p> <p><a href="/sites/default/files/inline-images/GuyTeeshirtGlassesDesk.png"><img alt="Guy in tee-shirt at desk" data-entity-type="file" data-entity-uuid="56cbb1fe-752c-4e25-9188-381c2d4e9b47" hspace="10" src="/sites/default/files/inline-images/GuyTeeshirtGlassesDesk.png" width="200" class="align-right" /></a>Besides using prompts, AI image generation tools also let you base your new image on a theme or model. These next two images were generated using a similar prompt but different models; the one on the right uses one called, "Dream Studio" while the one on the left uses "Nova Flux 2024" (which is ArtSpace's newest and most realistic model).</p> <p><a href="/sites/default/files/inline-images/OldManExasperated.png"><img alt="Old and exasperated" data-entity-type="file" data-entity-uuid="aad7cdc6-5144-4ce0-90ff-52bd7f4f8dae" hspace="10" src="/sites/default/files/inline-images/OldManExasperated.png" width="200" class="align-left" /></a></p> <p>Another factor at ArtSpace.ai, the tool I used for generating all the images on this page, is the <em>seed</em>. This can either be a number or (on some models), an image. I have not had success using an image to teach it "likeness" yet, I'm still learning the nuances of this technology. What is interesting is how very different images can be generated from the same prompt by simply using a different seed number. The following images were all generated using the same model and prompt but with one digit different in a 10-digit seed number:</p> <p> </p> <table align="center" cellpadding="5"> <tbody> <tr> <td><a href="/sites/default/files/inline-images/BWSpiralColorTinged_Seed3949969084.png"><img alt="Black &amp; White Spiral with Color Tinges #1" data-entity-type="file" data-entity-uuid="10f31403-a6d4-4252-94cd-7c5e7b3a221c" src="/sites/default/files/inline-images/BWSpiralColorTinged_Seed3949969084.png" width="160" class="align-center" /></a></td> <td><a href="/sites/default/files/inline-images/BWSpiralColorTinged_Seed3949969085.png"><img alt="Black &amp; White Spiral with Color Tinges #2" data-entity-type="file" data-entity-uuid="63a788f6-b35a-49fb-89ed-7125968eab7e" src="/sites/default/files/inline-images/BWSpiralColorTinged_Seed3949969085.png" width="160" class="align-center" /></a></td> <td><a href="/sites/default/files/inline-images/BWSpiralColorTinged_Seed3949969086.png"><img alt="Black &amp; White Spiral with Color Tinges #3" data-entity-type="file" data-entity-uuid="c0c16cd1-5c06-41cb-9c97-75a4d9e638c5" src="/sites/default/files/inline-images/BWSpiralColorTinged_Seed3949969086.png" width="160" class="align-center" /></a></td> <td><a href="/sites/default/files/inline-images/BWSpiralColorTinged_Seed3949969087.png"><img alt="Black &amp; White Spiral with Color Tinges #4" data-entity-type="file" data-entity-uuid="26cadeb0-3795-459e-9375-a5fb012b8f44" src="/sites/default/files/inline-images/BWSpiralColorTinged_Seed3949969087.png" width="160" class="align-center" /></a></td> </tr> </tbody> </table> <p><a href="/sites/default/files/inline-images/BikiniGirlOnDock.png"><img alt="Bikini girl on boat dock with third arm" data-entity-type="file" data-entity-uuid="f040d8fe-b296-4ba8-84dd-a2e62d3b0e57" src="/sites/default/files/inline-images/BikiniGirlOnDock.png" width="175" class="align-right" /></a>AI image generators are not perfect. It can sometimes take several iterations of modifying your prompt to get the image you want; and it may actually lead you down a rabbit hole of exploration in a direction you did not expect. If you have a tool that lets you explore without time or cost limits, that can be a fun distraction. But it can be frustrating if you don't have the time to explore and don't know why the computer can't read your mind! I've seen a lot of example prompts that are long and explicit and generate amazing artwork. Learning to talk so the computer generates the image you want is a fine-tuned craft. It's all too easy to confuse the AI with text that seems innocent and clear to us but causes the generator to place an arm or shadow in an impossible position or not give enough fingers on a hand. I generated one of a woman in a bikini on a boat dock but in one of the iterations added the phrase, "motioning to join her in the water" and the AI added a third arm.</p> <p><a href="/sites/default/files/inline-images/LargeBlueMotorcycle.png"><img alt="Large blue motorcycle" data-entity-type="file" data-entity-uuid="d1e7b9ab-322c-456c-a320-d1678044f133" hspace="5" src="/sites/default/files/inline-images/LargeBlueMotorcycle.png" width="300" class="align-left" /></a>AI image generators can also be used to envision products that don't yet exist or describe a home remodel taking shape or how new furniture would look in your living room. Colors and sizes and shapes can be changed and the image regenerated. I saw a video recently how retailers are using AI to help customers visualize themselves in new clothing or a different pair of glasses. To the left is an image created from the prompt, "large, futuristic dark blue motorcycle with chrome exhaust pipes, sitting in a showroom, oversize rear tire, blurry background". With a several more lines of descriptive text, an engineer could see potential problems with a design before spending hours in a CAD program.</p> <p>You may have been playing around with some free tools or perhaps you've subscribed to an AI service to help in your programming and it happens to provide an image generator as well.  If you are still looking, I invite you to check out <strong>ArtSpace.ai</strong> which is one I discovered recently. It seems to be pretty good, has a lot of variety, the limit of 500 images a month is plenty for me, it's fairly quick (about 11 seconds per image), and I can download them from the web browser interface and use them any way I want--even commercially! Most software tools these days have a monthly or annual cost to use them and <a href="https://www.artspace.ai/?ref=delphidave">ArtSpace.ai</a> has both; but they also have a seldom-seen <em><a href="https://www.artspace.ai/lifetime?ref=delphidave">one-time-price</a></em> (which is just a little more than the annual price). For me, it was just too irresistible!</p> <a href="/sites/default/files/inline-images/CastleComputer.png"><img alt="Computer in the shape of a sandcastle" data-entity-type="file" data-entity-uuid="594148b8-b601-433e-9a0c-2b6b95291709" src="/sites/default/files/inline-images/CastleComputer.png" width="600" class="align-center" /></a> <p> </p> </div> <section id="node-comment"> <article data-comment-user-id="0" id="comment-642" class="js-comment single-comment"> <header class="comment-user-picture"> <article typeof="schema:Person" about="/user/0"> </article> <mark class="hidden" data-comment-timestamp="1725840658"></mark> </header> <div class="single-comment-content-body"> <h3 class="single-comment-title"><a href="/comment/642#comment-642" class="permalink" rel="bookmark" hreflang="en">Wow! Thanks for sharing this…</a></h3> <div class="single-comment-meta"> <span><a rel="nofollow" href="https://capecodgunny.blogspot.com/" lang="" typeof="schema:Person" property="schema:name" datatype="">Michael Riley (not verified)</a> Sun, 09/08/2024 - 16:13</span> </div> <!-- /.single-comment-meta --> <div class="single-comment-content"> <div class="field field--name-comment-body field--type-text-long field--label-hidden field-item"><p>Wow! Thanks for sharing this David.</p></div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=642&amp;1=default&amp;2=en&amp;3=" token="1h2tRUUsHHoZdPPIiFRWZDY3oWPVG-jAmDuprIIg6mQ"></drupal-render-placeholder> </div> </div> <!-- /.single-comment-content --> </article> <div class="indented"> <article data-comment-user-id="1" id="comment-643" class="js-comment single-comment"> <header class="comment-user-picture"> <article typeof="schema:Person" about="/user/1"> </article> <mark class="hidden" data-comment-timestamp="1725844279"></mark> </header> <div class="single-comment-content-body"> <h3 class="single-comment-title"><a href="/comment/643#comment-643" class="permalink" rel="bookmark" hreflang="en">Not directly related to Delphi</a></h3> <div class="single-comment-meta"> <span><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span> Sun, 09/08/2024 - 18:09</span> <p class="visually-hidden">In reply to <a href="/comment/642#comment-642" class="permalink" rel="bookmark" hreflang="en">Wow! Thanks for sharing this…</a> by <a rel="nofollow" href="https://capecodgunny.blogspot.com/" lang="" typeof="schema:Person" property="schema:name" datatype="">Michael Riley (not verified)</a></p> </div> <!-- /.single-comment-meta --> <div class="single-comment-content"> <div class="field field--name-comment-body field--type-text-long field--label-hidden field-item"><p>You're welcome, glad you liked it!</p> <p>I debated whether to write about this or not since it's not directly related to Delphi programming but I've been having fun and as soon as I started generating images, ideas came to mind about how these could be used to enhance blogs, to illustrate ideas, or come up with a cool, new avatar (like the one at the top of this page!).</p> <p>But so far, it's just for fun; for example, I play board games with some friends and generated this one yesterday:</p> <a href="/sites/default/files/inline-images/FourFriendsBoardGaming-2sm.png" rel="nofollow"><img alt="Four friends playing a board game" data-entity-type="file" data-entity-uuid="3bcd371a-7507-4ad2-a585-a2c187c22f58" src="/sites/default/files/inline-images/FourFriendsBoardGaming-2sm.png" width="400" class="align-center" /></a> <p> </p></div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=643&amp;1=default&amp;2=en&amp;3=" token="LxTm7r2u-N98SpPsBwBoCqQ_p_TWWHgvjEn2x0EwYyY"></drupal-render-placeholder> </div> </div> <!-- /.single-comment-content --> </article> </div> <div class="comment-form-wrap"> <h2 class="add-comment-title"><i class="icon-add_comment theme-color"></i> Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=187&amp;2=comment_node_blog&amp;3=comment_node_blog" token="XcMmQYM7Q3mYqYC3aQG6ZfroXIoHWKIA_7gz6HJruL8"></drupal-render-placeholder> </div> <!--/.comment-form --> </section> Fri, 06 Sep 2024 06:20:56 +0000 david 187 at http://corneliusconcepts.tech GExperts - A Most Awesome Delphi Companion http://corneliusconcepts.tech/gexperts-most-awesome-delphi-companion <span property="dc:title">GExperts - A Most Awesome Delphi Companion</span> <div class="node-taxonomy-container"> <h3 class="term-title"><i class="icon-hashtag theme-color"></i> Category</h3> <ul class="taxonomy-terms"> <li class="taxonomy-term"><a href="/programming" hreflang="en">Programming</a></li> </ul> </div> <!--/.node-taxonomy-container --> <span rel="sioc:has_creator"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span></span> <span property="dc:date dc:created" content="2024-08-23T23:07:41+00:00" datatype="xsd:dateTime">Fri, 08/23/2024 - 16:07</span> <div property="content:encoded" class="field field--name-body field--type-text-with-summary field--label-hidden field-item"><p><img alt="GExperts Wizard" data-entity-type="file" data-entity-uuid="39aad722-770f-415a-b374-3be372eadad8" src="/sites/default/files/inline-images/gexperts_wizard.png" class="align-right" />GExperts is one of the oldest and probably the most well-known plug-in for the Delphi IDE. I've been using it since the early versions and still use it in Delphi 12 today. Some of its features have been incorporated into the IDE itself over the years but it continues to evolve and be a useful productivity aid. </p> <p>In case you don't know what GExperts is, the <a href="https://www.gexperts.org/">website</a> describes it as "a free set of tools built to increase the productivity of Delphi and C++Builder programmers by adding several features to the IDE." I agree and would go on to say it has so many features that there must be something beneficial for everyone!</p> <p>Here is a list, in no particular order, of how I personally have used GExperts to improve my productivity over the last 20+ years. And yes, I have used every one of the features listed below at one time or another; and no, this is NOT all the features!</p> <h2>Code Proofreader</h2> <p>When working in early versions of Delphi (e.g. 5 and 7, 2007), the <em>Code Proofreader</em> not only helped keep identifiers cased consistently but can also save lots of time correcting typos with its AutoCorrect list. How many times have you typed <code>;=</code> when you intended to type <code>:=</code> ? Or "<code>ned</code>" when you meant "<code>end</code>"? GExperts will fix that for you on-the-fly. </p> <p>But you can expand that idea to use it as a macro replacement, somewhat like a simplified version of today's <a href="https://corneliusconcepts.tech/delphi-productivity-tips-live-templates">Live Templates</a> in Delphi. For example, I've added "<code>ef</code>" as an auto-correct with its replacement value set to "<code>:= False;</code>" and "<code>et</code>" with a replacement value of "<code>:= True;</code>". This speeds up initializing Boolean variables significantly. I've added several other similar macros for other common code snippets.</p> <h2>Code Librarian</h2> <p>If you find yourself writing a bunch of programs that use similar techniques, such as accessing a plug-in architecture or an obtuse interface or even just a common SQL statement, and it requires writing similar code over and over, you might find yourself opening old programs or saving snippets of code in a text file you keep close by. But what if that code could be organized in a hierarchical list and built into Delphi, accessed with a few clicks and pasted right in your code with a hotkey? Well, GExperts does just that with its <em>Code Librarian</em> feature. What's more, this is shown in a floating window that can be moved to a second monitor for even more convenience and has several sample snippets to jump-start your collection. It has keyword search and color syntax highlighting. Once you find your code snippet, just click <code>Ctrl+Shift+V</code> to insert that block of code right into the Delphi editor.</p> <h2>ASCII Chart</h2> <p>Have you ever needed to look up the ASCII number for a character? With GExperts installed, simply select <em>ASCII Chart</em> from its menu and you'll instantly have a handy reference to all 256 characters, plus you can select what font and size to see them with.</p> <h2>Backup Project</h2> <p>Even with source control projects in place, I occasionally use the <em>Backup Project</em> feature of GExperts. Usually, this is for copying the project and all the units it uses to a temporary folder or to zip up for some reason, like to send to a friend. This feature saves time by collecting all the units referenced in the DPR of the Delphi project, allowing you to modify the list of files, then creating a .zip file of your choice with the files at the click of a button. That's easier than manually selecting the files in Windows Explorer, isn't it?</p> <h2>Editor Experts </h2> <p>There are several features that are only active while working in the code editor. They use configurable shortcut key combinations that, once learned, add several convenient tricks. Here are three of them:</p> <h3>Expert: Insert Date/Time</h3> <p>One company I worked for required all changes to code to be marked with a commented date accompanied by a "doc header" section at the top of the unit with that date and a comment about what ticket this resolved. It got tedious manually typing something like <code>{ 03/15/24 }</code> at the end of every line you where you made changes that day but with GExperts configured to use the date format I need, this became as simple as hitting my configured shortcut key combination, <code>Ctrl+Alt+A</code>!</p> <p><a id="ReverseStatement"></a></p> <h3>Expert: Reverse Statement</h3> <p>Have you ever typed up several statements to write configuration settings to an .INI file, then had to retype a bunch more similar ones to read the same settings? Perhaps you've copied, then pasted one set, then manually switched the assignments around. With GExperts, you can save a lot of copy/move/paste operations. Just select the pasted second set of statements and hit <code>Shift+Alt+R</code> (or your assigned shortcut keys) and all those assignment statements are switched around for you. Of course, you'll have to rename the <code>Read</code> statements to <code>Write</code> statements but select the block and use Find/Replace (or better yet, switch to <code>Sync Edit Mode</code> in modern versions of Delphi) to do that.</p> <h3>Expert: Comment/Uncomment</h3> <p>Before Delphi had the comment/uncomment hotkey, GExperts provided this feature, albeit not quite as smart. Instead of a toggle, there was one shortcut to comment (<code>Ctrl+Alt+.</code>) and another to uncomment (<code>Ctrl+Alt+,</code>). It doesn't detect whether or not the lines are already commented, still, this is another nifty time saver when you're in the code in an old version of Delphi.</p> <h2>Replace Components</h2> <p>There have been numerous times when I've wanted to replace one component or set of components with another. Like when changing all buttons on a form to <code>TBitBtn</code> in order to add button icons. You can always manually edit the DFM file and use Find/Replace from one component type to another then acknowledge the changed properties and correct the source's reference when saving the unit but there's a much easier way: select <em>Replace Components</em> from the GExperts menu while one of the button components is selected, select the new component type in the drop-down list of the pop-up window, check the desired Scope (such as "All components on the current form"), then watch as GExperts makes the change for you in a flash!</p> <h2>Components To Code</h2> <p>Most of the time, a component can be simply placed on a form or data module and you can let the framework manage the creation and destruction of the component for you, also allowing you to set properties conveniently at design-time. There have been several instances where I've started down that path, then realized I need to create the component in code, either because I was converting a data module to a component and didn't want to worry about form resources; or when I needed to make multiple copies of the component dynamically.</p> <p>Setting up all the properties at design time is simple, but can be tedious to do it with code when there are a lot of properties. Here again, GExperts can simplify your life; just select the component you've set up on the form or data module, select <em>Components to Code</em> from the GExperts menu, and it will put all the Delphi statements to create the code on the Clipboard so you can then paste the lines into your code wherever you need.</p> <h2>To Do List</h2> <p>I'm pretty sure that many features of GExperts have inspired features now a standard part of Delphi--and I'm pretty sure that the <em>To Do List</em> is one of those. It's an ingenious but simple idea that if a curly-braced comment is started with <code>TODO:</code>, a list can be generated from comments in all the current source files to provide you a centralized view of tasks. This is another feature with a floating window, and this one can be docked or moved to a second monitor. It functions nearly identical to Delphi's TODO list in modern versions of the IDE.</p> <h2>Message Dialog Wizard</h2> <p>Have you ever added a <code>MessageDlg</code> statement in your code and then at runtime realized it didn't look quite right or you inadvertently switched the <code>Caption</code> with the <code>Title</code>? With GExperts <em>Message Dialog</em> wizard, not only can you build your message dialog statement visually but you can also preview it before a line of code is written! But wait there's more--you can also switch to using <code>MessageBox</code> instead or opt to embed the whole thing in an <code>IF</code> or <code>CASE</code> statement that checks its result! How convenient is that?</p> <h2>Set Tab Order</h2> <p>If you have several controls on a form and add and/or rearrange them over time without setting their tab order, running the program at runtime can be frustrating for a user who tries to navigate among them using the Tab key. Paying attention to the tab order of the various components can increase usability significantly. While new versions of Delphi have a feature to reset the ordering for you, the one in GExperts has more features--and has been around a very long time.</p> <h2>Configuration</h2> <p>There are some parts of GExperts that don't write code or provide real-time functionality but simply improve Delphi's IDE in subtle ways. Here are a couple of areas I'd like to point out.</p> <h3>Config: Component Palette</h3> <p>If you use an early version of Delphi and have lots of components on your horizontally positioned component palette, it can be difficult to find the right tab for the component set you're looking for because you have to slowly scroll through the component tabs until you find the right one. GExperts provides an option to add tabs on multiple lines so you can see all the component tabs at once. Of course, this takes more vertical space so you have to adjust (and save) the editor layout but this is a huge convenience when working with forms and data modules in old versions of Delphi with lots of third-party components installed. If you don't like multi-line component tabs, GExperts has another option to add a pop-up window with all the tabs listed (and alphabetically, if you like); no more component tab scrolling! (This feature is not needed in newer versions of Delphi and was removed from GExperts.)</p> <h3>Config: IDE</h3> <p>GExperts has grown and added features over the years to accommodate new capabilities of Delphi. The <em>IDE</em> tab of the Configuration is one area that has changed a lot; some of the new options are simple niceties, like "<em>Enhance IDE dialogs</em>" that lets you resize and save the position of some of the fixed dialogs.</p> <p>Others can save lots of time, like one I've recently discovered, "<em>Enhance Build Events dialog with Favorites</em>". This is a huge time-saver for me because I use Build Events a lot and often copy them from project to project. Before finding this feature, I would often close the current project, open an old project that used the build event I needed, go into <em>Project Properties</em> &gt; <em>Build Events</em> and copy the command, then close and reopen the project I was working on and go back into Build Events to paste the command. Now, if I think I'll ever use a Build Event command again, I can just add it to my Favorites list and then select it later when I'm working on a different project, saving me many steps. This has become another of my new favorite productivity boosters!</p> <p>Look through these options and the other Configuration tabs--you may find some features I have not mentioned that work to solve a tedious task for you!</p> <h2>Copy Your Configurations</h2> <p>Let's say you've added several <em>Code Librarian</em> snippets, added several <em>Dictionary Words</em> to the <em>Code Proofreader</em>, and maybe created a few entries in <em>Favorite Files</em> (another feature of GExperts but not mentioned here), and now you are upgrading Delphi or using a different version of Delphi to work on another project and you want all your GExperts settings you've taken time to customize. The settings for GExperts have had very few changes over the years and are (mostly) compatible with each other (and when the format changes, there's usually an automatic conversion). This means you could copy the configuration files GExperts uses from one installation to another in order to keep your settings.</p> <p>These configuration files are not a secret. Just look in GExperts <em>Configuration</em>; in old versions, go to the <em>File Locations</em> tab for the directories; in newer versions, go to the <em>General</em> tab and the folders will be shown in the <em>File Locations</em> group box. These folders can be changed if you need; in fact, you may experiment with putting them on a network or synchronized folder.</p> <h2>Closing &amp; Acknowledgements</h2> <p>There are many more features of GExperts not mentioned here, like the Class Browser, Clipboard History, PE Information, Component Grid, and especially several Editor Experts I have not taken the time to learn. Look through the menus, read the documentation, and use the capabilities that you find most useful. There may be some tricks you had not thought of that simplify your coding experience in surprising ways. Some features have been removed from GExperts over the years as the IDE has changed and no longer supports functionality in the same way; others have identical functionality in the IDE so I no longer use the GExperts version of that feature. Still, I use old versions of Delphi enough, and there are still enough productivity gems for the most recent version of Delphi, that this plug-in is still the most valuable Delphi plug-in for me.</p> <p>I would like to thank <a href="https://blog.dummzeuch.de/experimental-gexperts-version/">Thomas Müller</a> for continuing the support of GExperts after the original programmer, Erik Berry, stopped supporting the project a few years ago. It's an open-source project and you can contribute suggestions and  ask questions on <a href="https://en.delphipraxis.net/forum/31-gexperts/">Delphi PRAXiS forum dedicated to this product.</a></p> <h2><a href="https://en.delphipraxis.net/forum/31-gexperts/">See the Webinar</a></h2> <p><a href="https://en.delphipraxis.net/forum/31-gexperts/">Finally, there was a </a><a href="https://www.youtube.com/live/4tR95Wi4a18?si=z41oGAPmldit5dUF">webinar on GExperts</a> by Jim McKeeth of GDK Software that demonstrated many of these features--and a few others I have not used. They will be presenting other popular Delphi plug-ins in the near future, so be sure and visit their "<a href="https://mailing.gdksoftware.com/webinars">webinars page</a>" to learn more.</p> </div> <section id="node-comment"> <article data-comment-user-id="0" id="comment-624" class="js-comment single-comment"> <header class="comment-user-picture"> <article typeof="schema:Person" about="/user/0"> </article> <mark class="hidden" data-comment-timestamp="1724469181"></mark> </header> <div class="single-comment-content-body"> <h3 class="single-comment-title"><a href="/comment/624#comment-624" class="permalink" rel="bookmark" hreflang="en">Totally Agree.</a></h3> <div class="single-comment-meta"> <span><span lang="" typeof="schema:Person" property="schema:name" datatype="">Ian (not verified)</span> Fri, 08/23/2024 - 19:16</span> </div> <!-- /.single-comment-meta --> <div class="single-comment-content"> <div class="field field--name-comment-body field--type-text-long field--label-hidden field-item"><p>Absolutely concur.  It is always the first Plug-In I install..</p></div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=624&amp;1=default&amp;2=en&amp;3=" token="z8h0Q9_fj0Q2USdvtspLTQNxC_iJZwi4OGxxt9rV_t4"></drupal-render-placeholder> </div> </div> <!-- /.single-comment-content --> </article> <div class="comment-form-wrap"> <h2 class="add-comment-title"><i class="icon-add_comment theme-color"></i> Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=186&amp;2=comment_node_blog&amp;3=comment_node_blog" token="FDAKAXAEMxoDiklNGN5VjPJgsoUQrUQE6QW_Lszgu7s"></drupal-render-placeholder> </div> <!--/.comment-form --> </section> Fri, 23 Aug 2024 23:07:41 +0000 david 186 at http://corneliusconcepts.tech Take Control of Your Delphi Project http://corneliusconcepts.tech/take-control-your-delphi-project <span property="dc:title">Take Control of Your Delphi Project</span> <div class="node-taxonomy-container"> <h3 class="term-title"><i class="icon-hashtag theme-color"></i> Category</h3> <ul class="taxonomy-terms"> <li class="taxonomy-term"><a href="/programming" hreflang="en">Programming</a></li> <li class="taxonomy-term"><a href="/windows" hreflang="en">Windows</a></li> </ul> </div> <!--/.node-taxonomy-container --> <span rel="sioc:has_creator"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span></span> <span property="dc:date dc:created" content="2024-06-22T02:06:03+00:00" datatype="xsd:dateTime">Fri, 06/21/2024 - 19:06</span> <div property="content:encoded" class="field field--name-body field--type-text-with-summary field--label-hidden field-item"><p>You can (and should) have complete control over all aspects of the version information, application icon, operating system compatibility, requested user access, Windows theming dependencies, and more in your Delphi projects!</p> <p>For the better part of a week, I struggled to get some of these aspects configured properly for a suite of programs that are all configured similarly and for which I was building a script to migrate and manage. I had to learn about Microsoft Manifest files and how they are linked into Delphi projects, where to list a common application icon and get it to show up in the Windows TaskBar consistently regardless of individual form settings, and how to manage version info centrally for these programs needing common information such as company and copyright but also per-project data like product name and version number. I've been programming for a long time in Delphi but had just been letting Delphi manage most of this information--sometimes with frustrating consequences.</p> <p><strong>It was past time to take control!</strong></p> <p>This blog lists three different types of resources that can be linked into your Windows application. It serves as both a reminder to my future self and a tutorial for others and pulls together a couple weeks' worth of research, trial and error, and scouring the web for bits of information.</p> <h2>APPLICATION ICON</h2> <p>In many VCL programs, simply selecting and assigning an .ICO file to the main form's Icon property, is all you need to set the icon of the application and to see it show up on the TaskBar when running. There are also project properties to set an icon but a main form's icon will override that; if your main form does not have an icon set, the project's icon will show on both the form and the TaskBar.</p> <p>However, if you need to override any defaults set by the project or a form, you can set the <code>Application.Icon</code> property in code at runtime. The two simplest ways to assign the icon is to call one of the Icon property's methods, such as <code>LoadFromFile</code> or <code>LoadFromResourceName</code>. The latter requires the icon to be stored either in the .EXE or in a loaded resource file such as a DLL. In my applications, I want to store the icon in the EXE itself to keep things simple. A convenient way to do that is to select <em>Project</em> &gt; <em>Resources and Images...</em> from the Delphi menu, click <em>Add</em>, then select your .ICO file, set the <em>Resource Type</em> to ICON, and give it a good <em>Resource Identifier</em> (I chose "APPICON").</p> <p>Now, when your application is running simply pass in <strong>HInstance</strong> (the handle to your application) and <strong>'APPICON'</strong> (or the name you chose for your <em>Resource Identifier</em>) to override the default icon:</p> <p><code>Application.Icon.LoadFromResourceName(HInstatnce, 'APPICON');</code></p> <p>The inclusion of this icon as a resource in your executable is recorded in the .DPROJ file for your project and the icon filename shows in the IDE's project manager alongside your code units; it triggers the resource compiler at compile time to generate and build a special resource file with the same base name as your project and with a ".dres" extension. When resources are added to your project like this, a line is also added to the .DPR file for your project:</p> <p><code>{$R *.dres}</code></p> <h2>VERSION INFO</h2> <p>There have been several blogs over the years mentioning the poor version management in the Delphi IDE. The advice is to never allow Delphi to control the version information but to always manage it yourself externally. How do you do this? First, in the project options, make sure "Include version information in project" is unchecked for all configurations. Then, create a resource source file (.rc) with your version information specified in the right format, call the resource compiler to build this resource, and add a <code>$R</code> compiler directive to link it in.</p> <p>That may sound like a lot of work but I would invite you to read <a href="https://blog.dummzeuch.de/dzprepbuild-for-delphi/">Thomas Müller's blog</a> and download his <strong>dzPrepBuild</strong> utility. Not only will it build your resource file in the right format for you but it is built to be used in automation scripts to keep it updated with incrementing version numbers and copyright years.</p> <p>Following the guidelines for using this tool, my version info resource filename was <code>&lt;ProjectName&gt;_Version.rc</code>. Thus, I added a Pre-Build event to the project to build the resource just before compilation:<br /> <code>"$(BDS)\bin\brcc32.exe" $(OUTPUTNAME)_Version.rc</code></p> <p>Finally, I added one line to the project's .DPR:<br /> <code>{$R *_Version.RES}</code></p> <p>Correction--I actually <em>replaced</em> one line in my project's .DPR: I replaced the <code>{$R *.RES}</code>, incorrectly thinking I no longer needed that line. My belief was that since I cut out the automatic version info and replaced it with my own manually built version info resource, this line was no longer needed. However, that's when I noticed a visual change in my VCL applications and started studying manifest files.</p> <h2>MANIFEST</h2> <p>I had spent a bunch of time researching resources and version info and setting up a script and was feeling pretty good about how projects were being upgraded from Delphi 5 to Delphi 12 with new, more standardized configurations and a modern look and feel. I had been showing progress to my colleagues each week and how much time we would be saving. Then, suddenly, I noticed a visual change in the toolbar of all the programs that had been compiled with these new automated scripts and linked resources.</p> <p>We're using the <code>RzToolbar</code> from the <a href="https://getitnow.embarcadero.com/KonopkaControls-290-7.0_For12.1/">Konopka Signature VCL Controls</a> package and set a gradient style on the toolbar to go from <code>clBtnFace</code> on the left to <code>clBtnShadow</code> on the right. With all the buttons set to transparent, the icons floated on the toolbar with no borders until you hover your mouse over them, giving a nice, clean and responsive look to the application. But now, the buttons had lost their transparency--all the buttons had a white-ish box, the ones on the far left closely matched the toolbar color but it was very obvious on the right side.</p> <img alt="Without Windows themes, buttons are not transparent." data-entity-type="file" data-entity-uuid="6449cb23-67fc-4aa6-83ac-dda0d15b2acf" src="/sites/default/files/inline-images/RzToolbarTest-WhiteBox.png" class="align-center" /> <p>I tried everything I could think of, double-checking the Transparency property of the buttons, checking and unchecking "Enable Runtime Themes" in the manifest section of Delphi's project options, changing DPI awareness, looking through form properties--all to no avail. My research led to a couple of articles about manifest management in Delphi and, like project management, read that I should create my own manifest file--the <a href="https://www.finalbuilder.com/resources/blogs/modifying-xml-manifest-files-with-finalbuilder">blog by Vincent Parrett</a> was especially helpful with explanations and examples.</p> <p>I had written a sample program to test the gradient toolbar and set the project properties to use a custom manifest file that I gleaned from the examples and modified. After lots of trial-and-error testing I finally got to the point where I could break, then fix the transparency issue with the toolbar buttons by simply changing a section of the XML in the manifest. By this time, I had learned the manifest is treated like other resources and linked in--and it uses the default project resource name which I had thought was only for version information. By simply adding that default <code>{$R *.RES}</code> line back in the .DPR, everything was working again. (I wish the <a href="https://docwiki.embarcadero.com/RADStudio/Athens/en/Manifest_File">documentation on Delphi's support for manifest files</a> had mentioned that.)</p> <img alt="With Windows themes supported in the manifest, transparency works fine." data-entity-type="file" data-entity-uuid="823d1958-16b9-4c3c-ab82-3dae35359af0" src="/sites/default/files/inline-images/RzToolbarTest-Transparent.png" class="align-center" /> <p>So, what was the key to getting transparency working in the program? It turns out that <strong>enabling support for Windows Themes</strong> (which is what this transparency feature needed) was simply requiring a dependency on Microsoft Common Controls so that it would use a newer version of the visual controls. This is mentioned on an obscure page in the DocWiki where it shows the <a href="https://docwiki.embarcadero.com/RADStudio/Sydney/en/API_(ApplicationName.manifest)">default manifest</a> provided by Delphi for Windows targets. I would not have had to create a custom manifest if I had simply left that <code>$R</code> directive in place; however, I now know a lot more about what you can do with a manifest file by having read through the Microsoft documentation on <a href="https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests">Application Manifests</a>. For example, you can require admin privileges for a program (in <code>&lt;requestedPrivileges&gt;</code>) and you can require Windows 10 or above (in <code>&lt;compatibility&gt;</code>); there are other fine-grained constraints you can add to your applications if you need by crafting a custom manifest.</p> <h2>Sample Project</h2> <p>To preserve my research and make permanent use of the sample program that helped me figure this out, I put it up on Github. It's called <a href="https://github.com/cascade-software-systems/RzToolbarTest">RzToolbarTest</a> and includes a project file with associated icon, a resource source (.rc) file to specify the version info, and a manifest file, all linked in using three <code>$R</code> directives. </p> <p>I hope to save you--and my future self--some time with better control of Delphi projects!</p> </div> <section id="node-comment"> <article data-comment-user-id="0" id="comment-586" class="js-comment single-comment"> <header class="comment-user-picture"> <article typeof="schema:Person" about="/user/0"> </article> <mark class="hidden" data-comment-timestamp="1719062408"></mark> </header> <div class="single-comment-content-body"> <h3 class="single-comment-title"><a href="/comment/586#comment-586" class="permalink" rel="bookmark" hreflang="en">Wow, somebody else is…</a></h3> <div class="single-comment-meta"> <span><a rel="nofollow" href="https://blog.dummzeuch.de" lang="" typeof="schema:Person" property="schema:name" datatype="">Thomas Müller (not verified)</a> Sat, 06/22/2024 - 02:05</span> </div> <!-- /.single-comment-meta --> <div class="single-comment-content"> <div class="field field--name-comment-body field--type-text-long field--label-hidden field-item"><p>Wow, somebody else is actually using my dzPrepBuild. I was under the impression that I was the only one.</p></div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=586&amp;1=default&amp;2=en&amp;3=" token="vRpWVXjOzT1w9z9NzwuhLROJxghWXdd3i01C0JsjC9c"></drupal-render-placeholder> </div> </div> <!-- /.single-comment-content --> </article> <div class="indented"> <article data-comment-user-id="0" id="comment-596" class="js-comment single-comment"> <header class="comment-user-picture"> <article typeof="schema:Person" about="/user/0"> </article> <mark class="hidden" data-comment-timestamp="1720831111"></mark> </header> <div class="single-comment-content-body"> <h3 class="single-comment-title"><a href="/comment/596#comment-596" class="permalink" rel="bookmark" hreflang="en">@Thomas Müller : I think…</a></h3> <div class="single-comment-meta"> <span><a rel="nofollow" href="https://www.emploiresto.com" lang="" typeof="schema:Person" property="schema:name" datatype="">Dan (not verified)</a> Fri, 07/12/2024 - 11:34</span> <p class="visually-hidden">In reply to <a href="/comment/586#comment-586" class="permalink" rel="bookmark" hreflang="en">Wow, somebody else is…</a> by <a rel="nofollow" href="https://blog.dummzeuch.de" lang="" typeof="schema:Person" property="schema:name" datatype="">Thomas Müller (not verified)</a></p> </div> <!-- /.single-comment-meta --> <div class="single-comment-content"> <div class="field field--name-comment-body field--type-text-long field--label-hidden field-item"><p>@Thomas Müller : I think congratulations are in order :-)</p></div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=596&amp;1=default&amp;2=en&amp;3=" token="_mRgKizOWz6MUVkpHeOJX8gngIRYYQDHOoPyMSkxJEE"></drupal-render-placeholder> </div> </div> <!-- /.single-comment-content --> </article> </div> <article data-comment-user-id="1" id="comment-587" class="js-comment single-comment"> <header class="comment-user-picture"> <article typeof="schema:Person" about="/user/1"> </article> <mark class="hidden" data-comment-timestamp="1719063770"></mark> </header> <div class="single-comment-content-body"> <h3 class="single-comment-title"><a href="/comment/587#comment-587" class="permalink" rel="bookmark" hreflang="en">Recently Found</a></h3> <div class="single-comment-meta"> <span><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span> Sat, 06/22/2024 - 06:42</span> </div> <!-- /.single-comment-meta --> <div class="single-comment-content"> <div class="field field--name-comment-body field--type-text-long field--label-hidden field-item"><p>I only found it a couple of weeks ago. It all started from a conversation on <a href="https://en.delphipraxis.net/topic/11605-product-delphi-parser-ai-claims-what-does-it-mean/" rel="nofollow">Delphi PRAXiS</a> when Jim McKeeth mentioned the <a href="https://docwiki.embarcadero.com/RADStudio/Athens/en/ReFind.exe,_the_Search_and_Replace_Utility_Using_Perl_RegEx_Expressions" rel="nofollow">reFind</a> utility which got me started down the path of automating Delphi project migration. I had a large script filled with regular expressions and it was going well but there was a hole in the process that required manual editing after converting a Delphi 5 project (with no .dproj) up to Delphi 12. I didn't want to rely on programmers setting up the version info manually as it would surely result in inconsistencies. Besides, it's just text and the script had so much of the project information available already--surely that could be automated as well? So, I went looking for a tool to fill that gap and found the perfect solution with yours! And it will also fit in nicely with plans to set up a build server. I just hadn't gotten around to mentioning it on the forum yet because I just finished it late yesterday. Thanks for writing that utility--and blogging about it!</p></div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=587&amp;1=default&amp;2=en&amp;3=" token="vZ7PSCpYuy14r-l6c9hadw3MoFBZCXhOZOuvroaEoOI"></drupal-render-placeholder> </div> </div> <!-- /.single-comment-content --> </article> <div class="comment-form-wrap"> <h2 class="add-comment-title"><i class="icon-add_comment theme-color"></i> Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=185&amp;2=comment_node_blog&amp;3=comment_node_blog" token="X6CQ9qEDJ2_jx__e-ZOBkzi-AY1ygHMBkLuqQQKUbkU"></drupal-render-placeholder> </div> <!--/.comment-form --> </section> Sat, 22 Jun 2024 02:06:03 +0000 david 185 at http://corneliusconcepts.tech Delphi as a Build Machine http://corneliusconcepts.tech/delphi-build-machine <span property="dc:title">Delphi as a Build Machine</span> <div class="node-taxonomy-container"> <h3 class="term-title"><i class="icon-hashtag theme-color"></i> Category</h3> <ul class="taxonomy-terms"> <li class="taxonomy-term"><a href="/programming" hreflang="en">Programming</a></li> </ul> </div> <!--/.node-taxonomy-container --> <span rel="sioc:has_creator"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span></span> <span property="dc:date dc:created" content="2024-05-09T02:07:58+00:00" datatype="xsd:dateTime">Wed, 05/08/2024 - 19:07</span> <div property="content:encoded" class="field field--name-body field--type-text-with-summary field--label-hidden field-item"><p>Building software projects involves, in addition to writing code, repetitive tasks like compiling, testing, signing, and deploying your completed application. Many times, a complicated suite of programs needs to be built in a certain order, copied to multiple locations, or even set up with special parameters for various customers. Large teams will likely have a dedicated DevOps person to handle these tasks, often with special "build machines" and complex scripts. Many development groups will utilize automation software to accomplish these tasks on a continual basis.</p> <p>As a Delphi developer, perhaps your organization has invested in <a href="https://www.finalbuilder.com/finalbuilder">FinalBuilder</a> or <a href="https://www.mitec.cz/pm.html">Project Maker</a> or maybe they set up a build server with <a href="https://codehunter.cc/a/jenkins/how-manage-to-get-an-automated-build-server-with-jenkins-for-projects-in-delphi">Jenkins</a> or even figured out how to get a <a href="https://ideasawakened.com/post/radauthenticator-part-4-build-automation-with-delphi-and-github-by-installing-a-self-hosted-runner">self-hosted runner for GitHub</a> working. These all take time and effort and a little money to establish; and while they pay off in the long-run, they may be just out of reach for your skillset or budget, or simply overkill for the small projects you're working on.</p> <p>But that doesn't mean you're stuck doing everything manually. Did you know you can perform many aspects of build automation right within the Delphi IDE on your own desktop? This is accomplished with three built-in features that work in conjunction with each other:</p> <ol> <li>Build Events,</li> <li>Project Groups, and</li> <li>Build Groups.</li> </ol> <p>Let's discuss each of these.</p> <h2><a href="https://docwiki.embarcadero.com/RADStudio/Athens/en/Build_Events">Build Events</a></h2> <p>A "build event" allows you to execute custom commands automatically as part of your build process. This can include tasks such as copying files, running scripts, or invoking external tools. There are two types of Delphi build events, <em>pre-build</em> and <em>post-build</em> events. As their names imply, pre-build events are commands that are run just before the project is built and post-build events run immediately afterwards. You can set flags so that errors returned from these commands halt further processing. For example, if a pre-build event tries to get the latest version of the code from a server but the server is unavailable, the build could be halted so you're not building something that might be out of date. Similarly, a post-build command could run a series of tests on the compiled project and if it fails, prevent further actions from taking place so that you can address the problem before sending out a faulty application. You can also define different build events for different platforms or configurations. For example, the debug executable might be copied to a network share for others to test while the release might get uploaded to a website. I often use build events to apply code-signing to release versions of executables.</p> <p>To configure build events in Delphi, <em>right</em>+<em>click</em> on your project in the Project Manager, select Options, then select Build Events under the "Building" section. You may also see "Pre-link events"; those are only applicable to C++ projects. </p> <img alt="Delphi's Build Event Editor" data-entity-type="file" data-entity-uuid="10725a11-397b-4158-9d66-f0da6816e6e4" src="/sites/default/files/inline-images/BuildEventEdit-SignTool.png" class="align-center" /> <p>Build event commands are executed by the IDE as if they were run on the DOS command line. You can add more than one command but they're stored concatenated with an ampersand (&amp;) in the project file which, in some older versions of Delphi are not handled properly (it also may cause parsing problems if you use an ampersand in your command). Many programmers write batch scripts that are called by the build event if they need to execute more than one command during a build event.</p> <p>One very nice feature of using Delphi's build events is having access to pre-defined macros in the IDE. For example, <code>$(OUTPUTDIR)</code> gets expanded to the folder where the compiled project will be placed and <code>$(Platform)</code> is the project's active platform name. These macros get replaced by their character values when the command is executed and can be used directly or passed in as a parameter to a script called by the build event. The build event editor shows the list of available macros in the bottom half of the window. </p> <p>In the screenshot above, I call a signing tool to code-sign the release version of a compiled program and pass in <code>$(OUTPUTPATH)</code>, which gets replaced with the completed executable's full path and filename when executed. If I change the project name or the path where compiled executables are placed, this build event command still works; also, I can copy this command to a different project and not have to change anything because I used a macro instead of hard-coding the filename. Note that I enclosed the macro in double-quotes because the path may include spaces or special characters that would cause parsing problems when run on the command-line.</p> <p>Debugging build events can be a little tricky; it's best to test the commands manually on the command line before putting them into a build script. When executed by a build process in Delphi, they appear in the "Output" tab of the "Messages" pane with their macros expanded.</p> <p>These automation capabilities really become useful when several projects are grouped together into a Project Group.</p> <h2><a href="https://docwiki.embarcadero.com/RADStudio/Athens/en/Projects_Window">Project Groups</a></h2> <p>One of the oldest features of Delphi's project management is the ability to group several projects together into a Project Group. A project group allows you to manage multiple related projects together without needing to unload one project, then load another. These projects can be sub-modules of a main application (e.g. DLLs or BPLs) or related programs possibly launched by the main application. In any case, it is useful to navigate between them when they're all visible in the same workspace.</p> <p>To create a project group in Delphi, simply load a first project like usual, then add an existing project to the group by right+clicking on the project group node in the Project Manager and selecting "Add Existing Project." You can add several projects to the list by repeating this process. You can then <em>right</em>+<em>click</em> on the project group name and change the name, compile or build all the projects with a simple mouse click.</p> <p><img alt="Add a project to a Project Group" data-entity-type="file" data-entity-uuid="e55f7a03-e082-43e6-8577-cda701daea72" src="/sites/default/files/inline-images/ProjGrp104.png" class="align-right" /></p> <p>When you select to compile or build all projects in this manner, the order of the projects can be important as compilation order of a "Build All" process always starts with the first project at the top and goes sequentially down the list. You may have projects that depend on others being compiled and available before they will successfully compile. You can adjust the order of the projects in the project group by right+clicking on a project and selecting either "Build Sooner" or "Build Later" from the pop-up menu to move the project up or down in the list.</p> <p>The buttons at the top of the Project Manager provide options for managing your project group. In addition to the convenience of having them lumped together for quick navigation, you can also change the configuration or platform of all the projects in the group at once. </p> <img alt="Setting the platform of all projects in a group at once" data-entity-type="file" data-entity-uuid="eb94998a-089c-4171-a309-a9cc00eaf84b" src="/sites/default/files/inline-images/ProjGrpPlatforms.png" class="align-center" /> <p><br /> <img alt="The button that opens the Build Groups pane" data-entity-type="file" data-entity-uuid="da5e864f-e8fa-4bdf-b7b3-a0c6000a3bcb" src="/sites/default/files/inline-images/BuildGroupsButton.png" class="align-right" />One of the Project Manager buttons takes this convenience and functionality one step further, it activates the Build Groups pane. </p> <h2><a href="https://docwiki.embarcadero.com/RADStudio/Athens/en/Build_Groups">Build Groups</a></h2> <p>When I first noticed Build Groups, I thought it was just another way to compile all the projects and so ignored it for quite some time. In one sense it is but after seeing a couple of articles on it a while back and exploring it further on my own, I realized there's additional value here that can take a developer's build process automation to the next level without leaving the IDE.</p> <p>Basically, a build group allows more flexibility when building all projects in a project group by allowing you to specify which projects to build; but more than that, it also allows you to select multiple configurations <em>and</em> platforms to use during a build process, meaning it can compile each project multiple times in one operation!</p> <blockquote> <p><em>Build Groups can compile each project multiple times in one operation!</em></p> </blockquote> <p>Let's say you need to build your suite of applications and modules for three different platforms; Build Groups can do that with one button click. Let's say you need to build all but one of your projects but you don't want to remove the project from the group; you can unselect any of the projects from the configured Build Group.</p> <p>When you activate the Build Groups pane, it shows all your projects and any defined build groups in a drop-down; initially there are no build groups but you can add, copy, and delete them with the toolbar buttons to the right of the list. When you add a build group, you give it a name and it creates a list of all the projects in your project group selected and with the current configuration and platform for each. You can then click in the space next to the configuration or platform for each listed project and click the button to add or remove various configurations or platforms. </p> <p>In the screenshot below, I've selected all projects in my project group, Debug and Release configurations for all but one of the projects, and various platforms for the different projects.</p> <img alt="Build group ready to generate 20 executables" data-entity-type="file" data-entity-uuid="2827e392-7360-45dd-acbf-b6af5c9a9fd7" src="/sites/default/files/inline-images/BuildGroupsVariousPlatforms20.png" class="align-center" /> <p>Once your build group is configured to your liking, you can click the appropriate button on the Build Groups toolbar to start the compile or build process, along with any defined build events for each selected project, then sit back and watch your computer work. If all goes well, clicking the <em>Compile</em> or <em>Build</em> buttons for this build group will generate 20 executables!</p> <h2>Conclusion</h2> <p>While this approach may not offer the sophistication of dedicated build automation tools, it can significantly reduce manual effort and ensure consistency in the build process. So, whether you're a lone coder or part of a team, consider harnessing the power of Delphi's Build Events and Build Groups to create a basic build automation system on your own desktop. By organizing projects effectively and automating repetitive tasks, you can streamline your development workflow to build better software, even on a shoestring budget.<br />  </p> </div> <section id="node-comment"> <div class="comment-form-wrap"> <h2 class="add-comment-title"><i class="icon-add_comment theme-color"></i> Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=184&amp;2=comment_node_blog&amp;3=comment_node_blog" token="vj77P6xK5_a-o8LGq2U45hUQ-475PUdfXisUzMdSe8s"></drupal-render-placeholder> </div> <!--/.comment-form --> </section> Thu, 09 May 2024 02:07:58 +0000 david 184 at http://corneliusconcepts.tech http://corneliusconcepts.tech/delphi-build-machine#comments Flexibility With Runtime Packages http://corneliusconcepts.tech/flexibility-runtime-packages <span property="dc:title">Flexibility With Runtime Packages</span> <div class="node-taxonomy-container"> <h3 class="term-title"><i class="icon-hashtag theme-color"></i> Category</h3> <ul class="taxonomy-terms"> <li class="taxonomy-term"><a href="/programming" hreflang="en">Programming</a></li> </ul> </div> <!--/.node-taxonomy-container --> <span rel="sioc:has_creator"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span></span> <span property="dc:date dc:created" content="2024-01-23T08:38:13+00:00" datatype="xsd:dateTime">Tue, 01/23/2024 - 00:38</span> <div property="content:encoded" class="field field--name-body field--type-text-with-summary field--label-hidden field-item"><p>Delphi's project options allows you to build a project with or without linking to runtime packages. A runtime package is a special dynamic-link library in the proprietary Delphi (or C++Builder) format that allows an executable to be split into various modules of functionality; these module files have a .BPL extension (for Borland Package Library). In the early days, this was useful to allow large applications to run on computers with limited memory because each module loads into its own memory space (these days, memory limitations are rare unless you're still deploying memory-hungry 32-bit applications). The main advantage today is realized if you have multiple executables that share some common functionality, like reporting or GUI controls or database access. The .EXEs can be smaller because instead of linking the reporting or GUI controls or database access code into each one of the .EXEs, you can ship that functionality in .BPLs and the .EXEs can be much smaller because they load that shared common code at runtime.</p> <p>To change this option, pull up your <em>Project Options</em> and find the "<strong>Link with runtime packages</strong>" option. If that option is not checked, you're building a monolithic executable--all the library code is in one big .EXE. But if you check that box and list the runtime packages for your applicaiton, your .EXE is smaller and you must ship the specified package files along with it. To be automatically found by the .EXE, the .BPLs must be either in the same folder as the .EXE or on the system PATH.</p> <blockquote> <p>What I didn't know is that you don't have to list all the runtime packages your application uses!</p> </blockquote> <p>I've known about this feature for many years and have used it at times for specific scenarios, or when a working with a plug-in system that used the .BPL format. What I didn't know, however, is that you don't have to list all the runtime packages for that option--and what packages you DON'T list, get linked into the .EXE. In fact, if you don't list ANY runtime packages, Delphi builds the application as if you had not checked the "Link with runtime packages" option at all!</p> <p>This means that you can determine how many of the packages to link in and which ones to leave out as extra .BPL files. My assumption has always been that it was all or nothing--either all packages were linked into one giant .EXE or all packages used by the application had to be listed and deployed for it to work. This is not so!</p> <p>By default, the "Link with runtime packages" option is unchecked for new application projects, allowing you to quickly create a stand-alone program. When you first check that option and enable the "Runtime packages" line, all the packages for all components you have installed in Delphi, whether from Embarcadero or open source or commercial products, and both VCL and Firemonkey, are all listed. You obviously don't need all these packages and cannot even build a project with everyone of them listed as they are not all available for all platforms; for example VCL and Firemonkey are incompatible in the same project and the BDE doesn't work on any platform other than Win32.</p> <p>If you're going this route with runtime packages and are deciding which ones to include, it may be prudent to stop periodically and build your project to see how big the .EXE size is. Depending on how many packages and .EXEs you have to ship, there will be a "sweet spot" that makes sense for you. A team I'm working with deploys different sets of several dozen .EXEs to hundreds of customers and while there could be an argument to save hard disk space, it's so cheap these days, it's hardly worth the time. What's more important for us is if we want to to send out a fix to a reporting library or upgrade the security of one of the internet components to support a new protocol or change a style of a GUI control. By keeping some of those types of .BPLs separate, we can update just a few files to affect all programs rather than recompile and redistribute hundreds of applications.</p> <p>After learning about this flexibility in listing runtime packages, I wanted to test it out and see some real numbers. So, I wrote a simple Win32 VCL application in Delphi 10.4 and placed a label, a button, and two third-party components on the main form, 1) a <a href="https://www.digital-metaphors.com/products" rel="nofollow">ReportBuilder</a> component, and 2) a non-visual layout saver from a <a href="https://github.com/corneliusdavid/ccLib" rel="nofollow">small library</a> I wrote. I built this four different ways, noting the size of the .EXE and the list of packages to deploy for each scenario:</p> <p>OPTION<strong>: NOT linked with runtime packages</strong><br /> EXE Size: <strong>26,690 KB</strong><br /> Deploy: single .EXE</p> <p>OPTION<strong>: Linked with runtime packages: only ccLib_R</strong><br /> EXE Size:<strong> 14,581 KB</strong><br /> Deploy: .EXE plus...</p> <ul> <li>rtl270.bpl</li> <li>vcl270.bpl</li> <li>dbrtl270.bpl</li> <li>cclib_r270.bpl</li> </ul> <p>OPTION<strong>: Linked with runtime packages, only dclRB2227 (ReportBuilder)</strong><br /> EXE Size:<strong> 4,224 KB</strong><br /> Deploy: .EXE plus...</p> <ul> <li>rtl270.bpl</li> <li>vcl270.bpl</li> <li>vclx270.bpl</li> <li>vclWinX270.bpl</li> <li>vclimg270.bpl</li> <li>vclie270.bpl</li> <li>vcledge270.bpl</li> <li>bindengine270.bpl</li> <li>rbRTL2227.bpl</li> <li>rbRCL2227.bpl</li> </ul> <p>OPTION<strong>: Linked with runtime packages, both dclRB227 and ccLib_R</strong><br /> EXE Size: <strong>4,210 KB</strong><br /> Deploy: .EXE plus...</p> <ul> <li>rtl270.bpl</li> <li>vcl270.bpl</li> <li>vclx270.bpl</li> <li>vclWinX270.bpl</li> <li>vclimg270.bpl</li> <li>vclie270.bpl</li> <li>vcledge270.bpl</li> <li>bindengine270.bpl</li> <li>rbRTL2227.bpl</li> <li>rbRCL2227.bpl</li> <li>ccLib_R.bpl</li> </ul> <p>Several things I noticed in this test: 1) removing my small library from the stand-alone .EXE dropped its size by quite a bit but only because it also separated out the RTL and VCL packages that had been previously linked in; 2) simply listing a single ReportBuilder package required <em>eight</em> additional .BPLs to also be shipped; 3) the real size of the "ccLib" package was realized in the last test as it only reduced the .EXE size by 14 KB from the previous test.</p> <p>A final consideration when selecting your package list is the hassle to package and deploy the various .BPL files; is it worth the extra management required to make sure all the .BPLs are deployed in order to save a few MB? The questions you have to answer will be different for your software and customer environment. But with modern build tools and automation, a lot of the details can be tucked away, never seen or dealt with after the configuration is tested so that is probably not a big deal.</p> <p>Well, if you didn't know this trick about varying the selected packages to include before, you now have a little extra flexibility for how you build your Delphi projects.</p></div> <section id="node-comment"> <div class="comment-form-wrap"> <h2 class="add-comment-title"><i class="icon-add_comment theme-color"></i> Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=183&amp;2=comment_node_blog&amp;3=comment_node_blog" token="7xKjMFhrQjG3YRM83t3BpwnCllSoMXFWRhGdAPdJDB4"></drupal-render-placeholder> </div> <!--/.comment-form --> </section> Tue, 23 Jan 2024 08:38:13 +0000 david 183 at http://corneliusconcepts.tech First Drupal-to-Hugo Conversion http://corneliusconcepts.tech/first-drupal-hugo-conversion <span property="dc:title">First Drupal-to-Hugo Conversion</span> <div class="node-taxonomy-container"> <h3 class="term-title"><i class="icon-hashtag theme-color"></i> Category</h3> <ul class="taxonomy-terms"> <li class="taxonomy-term"><a href="/cloud" hreflang="en">Cloud Computing</a></li> </ul> </div> <!--/.node-taxonomy-container --> <span rel="sioc:has_creator"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span></span> <span property="dc:date dc:created" content="2024-01-05T07:33:01+00:00" datatype="xsd:dateTime">Thu, 01/04/2024 - 23:33</span> <div property="content:encoded" class="field field--name-body field--type-text-with-summary field--label-hidden field-item"><p>Back in <a href="/major-drupal-upgrade-woes" rel="nofollow">May of 2022</a>, I wrote about a predicament I found myself in with multiple websites built in Drupal and the chore of upgrading in the right sequence to avoid incompatibilities with the installed version of PHP on the web server. I began to look around at my options and discovered others that also had <a href="https://andrewmarsh.com/blog/2016/02/08/moving-from-drupal-to-hugo/" rel="nofollow">fallen out of love with Drupal</a> were <a href="https://www.researchut.com/post/Drupal-7_to_Hugo/#gsc.tab=0" rel="nofollow">turning to static web page builders</a>, several of them <a href="https://tonisagrista.com/blog/2016/moving-to-hugo/" rel="nofollow">to Hugo</a>.</p> <p>This sounded very much like the adage, "What's Old is New Again!"  <em>But this time it's different.</em></p> <p>In the beginning, we all built our sites by hand with HTML. It wasn't long before that became quite tedious and we turned to scripting to add content to our sites, using such languages as Perl or PHP or maybe ASP.NET. This was, of course, much better than manually updating HTML pages, especially if you also maintained a table of contents, an index, and a site map. HTML editing tools proliferated.</p> <p>Then some people productized their scripting and <a href="https://kinsta.com/knowledgebase/content-management-system/" rel="nofollow">Content Management Systems</a> became a thing. This was even better because it allowed non-programmers to maintain their site and encouraged more features and multiple people and separation of code and data and design. All these were good things but as web sites got more complex and security issues crept in, these simple frameworks blossomed in size. Some of my early Drupal sites were less than 500 KB; the latest version of Drupal is nearly 120 MB and includes almost 20,000 files--and that's just the core install. After that, there are modules and themes and profiles and customizations--then security updates and bug fixes! IT NEVER STOPS!</p> <p>So, now I want to throw all that away and go back to hand-editing my content? Well, yes, actually!</p> <p>My <a href="https://nuez.io/posts/hugo-vs-drupal/" rel="nofollow">research</a> and testing led me to try out <a href="https://gohugo.io/" rel="nofollow">Hugo</a>; a year ago I wrote about this <a href="/new-direction-web-development" rel="nofollow">new direction</a> and during 2023, built a couple of small sites in Hugo for a client, <a href="https://modularbuildingsolutions.com" rel="nofollow">Modular Building Solutions</a>, and a personal photo album for our family. I wrote some utilities and really liked editing the simple <a href="https://www.howtogeek.com/448323/what-is-markdown-and-how-do-you-use-it/" rel="nofollow">Markdown</a> pages, running Hugo to rebuild the HTML pages, then uploading. In fact, with GitHub Actions, simply pushing updates to a remote repository can kick off an <a href="/automating-hugo-github-actions-raspberry-pi" rel="nofollow">automatic site update</a>.</p> <p>I was having fun again!</p> <p>Now the task of converting the content in my Drupal sites to Markdown pages lay in front of me. The first one I wanted to tackle was the <strong>Oregon Delphi User Group</strong> for which I am the coordinator. It's version of Drupal was fairly old and there were a couple of minor issues whenever I added meeting notices that I had to work around.  Plus, I didn't really like the theme but didn't feel like customizing it. You can see what it looked like on the <a href="http://web.archive.org/web/20230207021735/https://odug.org/" rel="nofollow">Wayback Machine</a>.</p> <p>So, over the holidays, I took some time, found a <a href="https://gist.github.com/amoutiers/991da535cc2afef8c1bb0c56326a378e" rel="nofollow">script</a> (or was it <a href="https://git.herzbube.ch/gitweb.cgi/tools.git/blob/HEAD:/drupal2hugo/drupal2hugo.php" rel="nofollow">this one</a>?), exported the content, and got to work. </p> <p>And yes, it <a href="https://waddles.org/2015/12/19/issues-converting-from-drupal-to-hugo/" rel="nofollow">took some work</a> to get all the content converted because the Drupal content editor allows different formats of text, including Full HTML, which I often used so I could customize parts of the entries or attach images and align them properly. The problem with that is it doesn't just store the text, it stores the HTML that I entered as well. So what I exported had to have its HTML tags removed and replaced with Markdown. There was probably an easier or more automated way but I had other edits to make as well. Instead of using Drupal's "Taxonomy" system to set up events by year, I just associated "year" tags to the various posts. Then, since I was adding tags anyway, I added a few other tags for speakers and topics. As I kept building and reviewing the finished site (which is nearly instantaneous with <a href="https://gohugo.io/commands/hugo_server/" rel="nofollow">Hugo's in-memory server</a> feature), I made other tweaks. Adding a theme was just as fun because I found <a href="https://gitlab.com/gabmus/hugo-ficurinia" rel="nofollow">one</a> that was easily modified to make the posts look like Pascal procedures!</p> <p>I unveiled it yesterday and so far, I've gotten positive feedback. I think it looks better than the old one, and it's MUCH faster because it's no longer running through a PHP interpreter to generate HTML, it just serves the pre-built HTML files. There's so much less to configure and, of course, there are NEVER any security concerns--but then again, it's just a static site that gets updated about once a month.</p> <p>Check it out: <strong><a href="https://odug.org" rel="nofollow">ODUG.ORG</a></strong>. And if you maintain a static site, you should try Hugo for yourself!</p></div> <section id="node-comment"> <article data-comment-user-id="0" id="comment-546" class="js-comment single-comment"> <header class="comment-user-picture"> <article typeof="schema:Person" about="/user/0"> </article> <mark class="hidden" data-comment-timestamp="1704505362"></mark> </header> <div class="single-comment-content-body"> <h3 class="single-comment-title"><a href="/comment/546#comment-546" class="permalink" rel="bookmark" hreflang="en">Good move. I also use Hugo…</a></h3> <div class="single-comment-meta"> <span><span lang="" typeof="schema:Person" property="schema:name" datatype="">Andre (not verified)</span> Fri, 01/05/2024 - 17:40</span> </div> <!-- /.single-comment-meta --> <div class="single-comment-content"> <div class="field field--name-comment-body field--type-text-long field--label-hidden field-item"><p>Good move. I also use Hugo and it feels good to have full control of your website. The website is also so lightweight and loads extreme fast compared to all bloated websites.</p> <p>Nice work and the website looks great 👍</p></div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=546&amp;1=default&amp;2=en&amp;3=" token="tl2ctT-mqRRDksqi8vrPNJAYpam6DBtYiL9aE4fRubk"></drupal-render-placeholder> </div> </div> <!-- /.single-comment-content --> </article> <div class="indented"> <article data-comment-user-id="1" id="comment-547" class="js-comment single-comment"> <header class="comment-user-picture"> <article typeof="schema:Person" about="/user/1"> </article> <mark class="hidden" data-comment-timestamp="1704505406"></mark> </header> <div class="single-comment-content-body"> <h3 class="single-comment-title"><a href="/comment/547#comment-547" class="permalink" rel="bookmark" hreflang="en">Thanks!</a></h3> <div class="single-comment-meta"> <span><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span> Fri, 01/05/2024 - 17:43</span> <p class="visually-hidden">In reply to <a href="/comment/546#comment-546" class="permalink" rel="bookmark" hreflang="en">Good move. I also use Hugo…</a> by <span lang="" typeof="schema:Person" property="schema:name" datatype="">Andre (not verified)</span></p> </div> <!-- /.single-comment-meta --> <div class="single-comment-content"> <div class="field field--name-comment-body field--type-text-long field--label-hidden field-item"><p>Thanks!</p></div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=547&amp;1=default&amp;2=en&amp;3=" token="UC-VqWiQNp_sfdDEkuvwWK_xd0aaqhOccVTNVRI71Gk"></drupal-render-placeholder> </div> </div> <!-- /.single-comment-content --> </article> </div> <article data-comment-user-id="0" id="comment-548" class="js-comment single-comment"> <header class="comment-user-picture"> <article typeof="schema:Person" about="/user/0"> </article> <mark class="hidden" data-comment-timestamp="1705083658"></mark> </header> <div class="single-comment-content-body"> <h3 class="single-comment-title"><a href="/comment/548#comment-548" class="permalink" rel="bookmark" hreflang="en">David, this is a fascinating…</a></h3> <div class="single-comment-meta"> <span><a rel="nofollow" href="https://capecodgunny.blogspot.com/" lang="" typeof="schema:Person" property="schema:name" datatype="">Michael Riley (not verified)</a> Wed, 01/10/2024 - 03:44</span> </div> <!-- /.single-comment-meta --> <div class="single-comment-content"> <div class="field field--name-comment-body field--type-text-long field--label-hidden field-item"><p>David, this is a fascinating article. After reading this I will definitely look into using Hugo.</p></div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=548&amp;1=default&amp;2=en&amp;3=" token="1nWieVkSqtXH2OH2OWPcDEYKWI3AIlRlZ8PAc0uTjlg"></drupal-render-placeholder> </div> </div> <!-- /.single-comment-content --> </article> <div class="comment-form-wrap"> <h2 class="add-comment-title"><i class="icon-add_comment theme-color"></i> Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=182&amp;2=comment_node_blog&amp;3=comment_node_blog" token="Cag3Q6Y2mLFN98X_SyM1qQa2otoYClqpngqRkpEZ6I4"></drupal-render-placeholder> </div> <!--/.comment-form --> </section> Fri, 05 Jan 2024 07:33:01 +0000 david 182 at http://corneliusconcepts.tech ChatGPT can write Delphi code http://corneliusconcepts.tech/chatgpt-can-write-delphi-code <span property="dc:title">ChatGPT can write Delphi code</span> <div class="node-taxonomy-container"> <h3 class="term-title"><i class="icon-hashtag theme-color"></i> Category</h3> <ul class="taxonomy-terms"> <li class="taxonomy-term"><a href="/programming" hreflang="en">Programming</a></li> </ul> </div> <!--/.node-taxonomy-container --> <span rel="sioc:has_creator"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span></span> <span property="dc:date dc:created" content="2023-12-29T17:46:55+00:00" datatype="xsd:dateTime">Fri, 12/29/2023 - 09:46</span> <div property="content:encoded" class="field field--name-body field--type-text-with-summary field--label-hidden field-item"><p>The company I work for has hundreds of small accounting programs written many years ago in Delphi 5 where they have been maintained ever since. As a new member of the team, my primary task is to help them upgrade to newer versions of Delphi. But I also assist in supporting their legacy code for dozens of county offices that rely on them every day. Sometimes, these customers need features that, while are simple to implement in newer versions of Delphi, were not yet added to the toolset in the late 1990s. One such request came through a few months ago and I was surprised to discover that it had never been requested--nor did the rest of the team know how to handle it.</p> <p>The need was to be able to import data through a SOAP webservice. I didn't learn about this until another programmer had the XML packet, complete with SOAP header he had pulled from the example sent to him, all ready packaged and ready to hand off to some other function. I knew I would be writing a DLL in Delphi 11 that would be called by his Delphi 5 application but I had never called a SOAP service from Delphi with raw XML--I had always used its WSDL importer, filled in properties and called a method of the class. As I looked further at this, trying to untangle what the end result of what I needed to do and thinking it couldn't be that hard, a thought struck me: why not try this new AI tool, ChatGPT, that others have said had been helping them with coding? I already had a free account and had used it a few times for research and to help explain a few technical things but this was a chance to see if it could actually help me save some time as a professional programmer.</p> <p>So I opened up a new ChatGPT session and typed in:</p> <blockquote> <p><strong>How do I call a SOAP web service from Delphi without using the WSDL importer?</strong></p> </blockquote> <p>The response was a great explanation of the process:</p> <blockquote> <p>Calling a SOAP web service from Delphi without using the WSDL importer involves manually crafting the SOAP request and handling the SOAP response using the Indy components (TIdHTTP) or other HTTP libraries. This approach requires a good understanding of SOAP XML structure and the HTTP protocol. Here's a basic outline of the steps you need to follow:</p> <ol> <li> <p><strong>Create the SOAP Request XML:</strong> Manually construct the SOAP request XML using the appropriate SOAP envelope, headers, and body for your web service method. The structure of the XML depends on the specific web service you're interacting with.</p> </li> <li> <p><strong>Set Up the HTTP Request:</strong> Use Delphi's HTTP library (e.g., TIdHTTP from Indy) to create an HTTP POST request to the web service endpoint. Set the appropriate headers, such as "Content-Type" and "SOAPAction" header.</p> </li> <li> <p><strong>Send the SOAP Request:</strong> Send the SOAP request XML as the content of the HTTP POST request. Make sure to encode the request XML properly.</p> </li> <li> <p><strong>Receive and Process the Response:</strong> Receive the HTTP response from the server. Extract the SOAP response XML from the response body.</p> </li> <li> <p><strong>Parse the SOAP Response:</strong> Manually parse the SOAP response XML to extract the relevant data. This may involve using XML parsing libraries like Delphi's built-in XML components or other third-party XML parsing libraries.</p> </li> <li> <p><strong>Handle Errors:</strong> Implement error handling logic to deal with various possible error scenarios, both at the HTTP level and the SOAP level.</p> </li> </ol> <p>Here's a simplified example of what the code might look like using Indy's TIdHTTP component:</p> <code>uses<br />   IdHTTP, IdSSLOpenSSL;</code><br /> <code>procedure CallSoapWebService;<br /> var<br />   HTTPClient: TIdHTTP;<br />   RequestXML: TStringStream;<br />   ResponseXML: string;<br />   EndpointURL: string;<br /> begin<br />   HTTPClient := TIdHTTP.Create(nil);<br />   RequestXML := TStringStream.Create('YourSOAPRequestXMLHere', TEncoding.UTF8);<br />   EndpointURL := 'YourWebServiceEndpointURLHere';<br /> <br />   try<br />     HTTPClient.Request.ContentType := 'text/xml;charset=UTF-8';<br />     HTTPClient.Request.CustomHeaders.Add('SOAPAction: YourSOAPActionHere');<br /> <br />     ResponseXML := HTTPClient.Post(EndpointURL, RequestXML);<br /> <br />     // Process the SOAP response XML here<br />   except<br />     on E: Exception do<br />     begin<br />       // Handle exceptions<br />     end;<br />   end;<br /> <br />   HTTPClient.Free;<br />   RequestXML.Free;<br /> end;</code> <p>Remember, this is a very basic example and may not cover all the complexities of real-world SOAP interactions. Additionally, manually dealing with SOAP can be error-prone and time-consuming, so using the WSDL importer is generally recommended for more robust and maintainable code.</p> </blockquote> <p>I was very impressed but since I'm using Delphi 11, I wanted to know if I could use the newer internet client components instead of Indy; so, I typed this in next:</p> <blockquote> <p><strong>Can I do this with NetHTTPClient instead?</strong></p> </blockquote> <p>The sample code was reworked to use this component instead and even acknowledged it provides an easier and more modern way to work with HTTP requests and responses.</p> <p>After playing around with the updated code for a bit, I noticed it had used the property <code>DefaultHeaders</code> which is not part of the <code>HTTPClient</code> component. I asked ChatGPT about that and it immediately apologized and reworked the code using <code>CustomHeaders</code> instead (why it couldn't have used the correct property in the first place is a mystery to me):</p> <pre> uses System.Net.HttpClient, System.Net.URLClient, System.Classes; procedure CallSoapWebService; var HTTPClient: TNetHTTPClient; RequestContent: TStringStream; ResponseContent: string; EndpointURL: string; begin HTTPClient := TNetHTTPClient.Create(nil); RequestContent := TStringStream.Create('YourSOAPRequestXMLHere', TEncoding.UTF8); EndpointURL := 'YourWebServiceEndpointURLHere'; try HTTPClient.CustomHeaders['Content-Type'] := 'text/xml;charset=UTF-8'; HTTPClient.CustomHeaders['SOAPAction'] := 'YourSOAPActionHere'; ResponseContent := HTTPClient.Post(EndpointURL, RequestContent).ContentAsString; // Process the SOAP response XML here except on E: Exception do begin // Handle exceptions end; end; HTTPClient.Free; RequestContent.Free; end; </pre> <p>I asked a few more clarifying questions and got really good answers, turned the procedure into a function, added a <code>try/finally</code> block around the bulk of the function to properly free the components, created an interface to convert strings to PChars to work inside the DLL (I suppose I could've asked ChatGPT to do this for me!), and ended up with a useful tool in our library to support our legacy code a little longer.</p> <pre> function CallSOAP(const RequestURL, RequestStr: string; var ResponseStr: string): Boolean; var RequestContent: TStringStream; begin NetHTTPClient := TNetHTTPClient.Create(nil); RequestContent := TStringStream.Create(RequestStr, TEncoding.UTF8); try Result := True; try NetHTTPClient.CustomHeaders['Content-Type'] := 'text/xml;charset=UTF-8'; ResponseStr := NetHTTPClient.Post(RequestURL, RequestContent).ContentAsString; except on E: Exception do begin ResponseStr := E.Message; Result := False; end; end; finally RequestContent.Free; end; end; </pre> <p>This is a really simple example of how AI can enhance programmer productivity.</p> </div> <section id="node-comment"> <article data-comment-user-id="0" id="comment-543" class="js-comment single-comment"> <header class="comment-user-picture"> <article typeof="schema:Person" about="/user/0"> </article> <mark class="hidden" data-comment-timestamp="1704231537"></mark> </header> <div class="single-comment-content-body"> <h3 class="single-comment-title"><a href="/comment/543#comment-543" class="permalink" rel="bookmark" hreflang="en">My experience was a lot worse</a></h3> <div class="single-comment-meta"> <span><a rel="nofollow" href="https://blog.dummzeuch.de" lang="" typeof="schema:Person" property="schema:name" datatype="">Thomas Müller (not verified)</a> Sat, 12/30/2023 - 09:25</span> </div> <!-- /.single-comment-meta --> <div class="single-comment-content"> <div class="field field--name-comment-body field--type-text-long field--label-hidden field-item"><p>I tried something similar several times but always got code that had some compile errors and / or was using some classes or methods that simply didn't exist. chatGPT always aplogized when I told it about these errors and generated new code, that again had the same kind of errors. After a few iterations of that I got bored and wrote the code myself.</p> <p>And after trying this several times with the same kind of results, I finally gave up.</p> <p>You seem to have had more luck or maybe your prompts were better than mine.</p></div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=543&amp;1=default&amp;2=en&amp;3=" token="oA8jCcM6B8oWb5ueyMESS9u6KweI2IXUq8CXMXqHEns"></drupal-render-placeholder> </div> </div> <!-- /.single-comment-content --> </article> <article data-comment-user-id="1" id="comment-545" class="js-comment single-comment"> <header class="comment-user-picture"> <article typeof="schema:Person" about="/user/1"> </article> <mark class="hidden" data-comment-timestamp="1704232070"></mark> </header> <div class="single-comment-content-body"> <h3 class="single-comment-title"><a href="/comment/545#comment-545" class="permalink" rel="bookmark" hreflang="en">To be fair...</a></h3> <div class="single-comment-meta"> <span><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span> Tue, 01/02/2024 - 13:47</span> </div> <!-- /.single-comment-meta --> <div class="single-comment-content"> <div class="field field--name-comment-body field--type-text-long field--label-hidden field-item"><p>To be fair, I encountered a compiler error as well after I switched to <code>NetHTTPClient</code>--it used <code>DefaultHeaders</code> instead of <code>CustomHeaders</code> but it was pretty quick to spot and fix. There may have been a couple other things as well, but the overall experience was that it increased my productivity in this instance by jump-starting the coding process--which is what I was hoping.</p> </div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=545&amp;1=default&amp;2=en&amp;3=" token="EuuYZuxnZKU0F0rWW43LDSlp25EfXBsIziQWmGnpUqg"></drupal-render-placeholder> </div> </div> <!-- /.single-comment-content --> </article> <div class="comment-form-wrap"> <h2 class="add-comment-title"><i class="icon-add_comment theme-color"></i> Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=181&amp;2=comment_node_blog&amp;3=comment_node_blog" token="r-jVbrYCWXuqe8Z7p4_mzNbFjTkrc4aDBXVsXBhiWiw"></drupal-render-placeholder> </div> <!--/.comment-form --> </section> Fri, 29 Dec 2023 17:46:55 +0000 david 181 at http://corneliusconcepts.tech