<?xml version="1.0" encoding="UTF-8"?><rss version="2.0">	<channel>	<title>TMS Software</title>	<link>https://www.tmssoftware.com</link>	<language>en</language>	<pubDate>Fri, 25 Sep 2026 10:33:00 +0100</pubDate>	<docs>http://blogs.law.harvard.edu/tech/rss</docs>	<ttl>1440</ttl>	<generator>TMS Software</generator>	<image>		<url>https://www.tmssoftware.com/site/img/tmslogo.png</url>		<title>TMS Software</title>		<link>https://www.tmssoftware.com</link>	</image>
	<item>
		<title><![CDATA[﻿Server Hosting in TMS Sparkle: Console, Windows Service or Linux Daemon]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2516</link>
		<author>Wagner Landgraf</author>
		<pubDate>Thu, 24 Sep 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		
		
		<p>Your REST API is finished. The endpoints work, the queries are fast, it runs beautifully when you press F9. Now put it in production.</p>

<p>That&#39;s usually where a Delphi server project gets complicated. The application that was so pleasant to debug has to become a <a href="https://learn.microsoft.com/en-us/windows/win32/services/services">Windows service</a> that starts at boot and runs with nobody logged in. So you add a second project with a <code>TService</code>, and now you maintain two versions of the same server - one you can debug and one you can ship. Somewhere in there appears a <code>{$IFDEF DEBUG}</code> that nobody wants to touch. And if the customer wants it on Linux, that&#39;s a third project.</p>

<p>None of that is your business logic. It&#39;s hosting: plumbing that has nothing to do with what your server actually does, but that stands between you and a deployed application.</p>

<p>The next version of <strong>TMS Sparkle</strong> removes it. It adds a <strong>hosting layer</strong> so that a server is <strong>one single project</strong>, one executable, that runs unchanged as a console application while you develop, as a Windows service in production, and as a Linux daemon. Three new IDE wizards create servers in this shape for Sparkle, XData and RemoteDB, so you start from it instead of building up to it.</p><p><br></p>

<h3>A quick refresher</h3>

<p><strong><a href="https://www.tmssoftware.com/site/sparkle.asp">TMS Sparkle</a></strong> is a high-performance HTTP server framework for Delphi. It&#39;s the foundation under <strong><a href="https://www.tmssoftware.com/site/xdata.asp">TMS XData</a></strong>, a framework for building REST APIs, <strong><a href="https://www.tmssoftware.com/site/remotedb.asp">TMS RemoteDB</a></strong>, a framework for remote database access over HTTP, <strong><a href="https://www.tmssoftware.com/site/sphinx.asp">TMS Sphinx</a></strong>, a full OAuth/OpenID Connect implementation to secure your APIs, among other products. Whatever you build with these products is, at the bottom, a Sparkle server, so everything in this article applies to all those.</p><p><br></p>

<h3>One project, three run modes</h3>

<p>The new unit <a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Host/index.html"><code>Sparkle.Host</code></a> turns a console program into a server host. The same binary runs:</p>

<ul>
  <li><strong>as an interactive console application</strong>, on Windows and Linux - what you get when you press F9;</li>
  <li><strong>as a Windows service</strong>, registered with the Service Control Manager by the executable itself;</li>
  <li><strong>as a <a href="https://systemd.io/">systemd</a> daemon</strong> on Linux, with a unit file the executable prints for you.</li>
</ul>

<p>The mode is chosen <strong>at runtime</strong>, by the command line. Not at compile time: no conditional defines, no separate service project, no <code>TService</code> descendant, no VCL form with Start and Stop buttons. One <code>.dproj</code> to build, debug, version and deploy.</p>

<p>What you deploy is exactly what you debugged. The host takes care of everything that differs between the three modes - the Service Control Manager handshake, POSIX signals, graceful shutdown, exit codes - and your server code never learns which mode it is in.</p><p><br></p>

<h3>Start with a wizard</h3>

<p>Choose <strong>File &gt; New &gt; Other</strong> in the IDE, and under <strong>Delphi Projects</strong> open the <strong>TMS BIZ</strong> category. All TMS BIZ wizards are now grouped there, including three that create a ready-to-run hosted server: <strong>TMS Sparkle Server</strong>, <strong>TMS XData Server</strong> and <strong>TMS RemoteDB Server</strong>.</p>

<p>Pick one and you get a console project targeting Win32, Win64 and Linux64, made of two files. The program file is one line of code:</p>

<pre><code>program XDataConsole;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  Sparkle.Host,
  ServerModuleUnit in &#39;ServerModuleUnit.pas&#39; {ServerModule: TDataModule};

begin
  RunHost(TServerModule);
end.
</code></pre>

<p>The second file is a data module with the server components already dropped and wired, ready for you to configure. That data module is your application: <a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Host/RunHost.html"><code>RunHost</code></a> creates it when the host starts and frees it when the host stops, so you write your endpoints and your logic there and ignore everything else.</p>

<p>Press F9. A console window opens:</p>

<pre><code>2026-09-11 10:15:02 [info] Now listening on: http://localhost:2001/tms/xdata
</code></pre>

<p>Open the address in a browser, press Ctrl+C to stop it. Requests in progress finish, the server shuts down cleanly, and you&#39;re back in the IDE. That&#39;s the whole development loop, and it is identical on Windows and on Linux.</p>

<p>The generated server uses the Sparkle <a href="https://doc.tmssoftware.com/biz/sparkle/guide/socket-server.html">socket server</a>, which behaves the same on both platforms and needs no administrative setup. If you&#39;d rather run on <a href="https://doc.tmssoftware.com/biz/sparkle/guide/server.html#httpsys-based-server">Http.sys</a> for a high-load Windows server, the generated code has the lines for it ready to uncomment.</p><p><br></p>

<h3>Deploy on Windows: one command</h3>

<p>There&#39;s no service project to build. The executable you have just been debugging installs itself. From an elevated command prompt:</p>

<pre><code>MyServer.exe --install
</code></pre>

<p>That&#39;s it. Your server is now a Windows service, set to start automatically at boot, and you manage it like any other:</p>

<pre><code>sc start MyServer
sc stop MyServer
</code></pre>

<p><code>MyServer.exe --uninstall</code> removes it. When it runs as a service there is no console to write to, so the host writes the log lines to a file next to the executable (or wherever you point it with <code>--log=</code>).</p>

<p>Run the executable with <code>--help</code> and it lists everything it accepts. No hand-written service registration, no <code>sc create</code> command line to get right, no separate installer step.</p><p><br></p>

<h3>Deploy on Linux: one pipe</h3>

<p>The same program writes its own <a href="https://systemd.io/">systemd</a> unit file:</p>

<pre><code>./myserver --systemd-unit | sudo tee /etc/systemd/system/myserver.service
sudo useradd -r myserver
sudo systemctl daemon-reload
sudo systemctl enable --now myserver
</code></pre>

<p>Four lines, and your server starts at boot, runs under its own user account and restarts if it fails. Nothing else is installed on the machine: no Apache, no web server in front, just your binary. <code>systemctl stop</code> sends <code>SIGTERM</code>, which becomes the same graceful shutdown as Ctrl+C, and the log goes to the journal:</p>

<pre><code>journalctl -u myserver -f
</code></pre>

<h3><br></h3><h3>Everything in one process</h3>

<p>A backend is rarely only HTTP. There&#39;s a nightly import, a queue to poll, a scheduler - things that used to justify yet another project and another service to install and monitor. The host runs them alongside your servers, in the same process, with the same lifetime:</p>

<pre><code>begin
  CreateHost
    .ServiceOptions(&#39;SalesApi&#39;, &#39;Sales API Server&#39;, &#39;REST API for the sales system&#39;)
    .Add(TApiServerModule)
    .Add(TReportsServerModule)
    .Add(TNightlyJobsWorker)
    .Run;
end.
</code></pre>

<p><a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Host/CreateHost.html"><code>CreateHost</code></a> takes any number of data modules and <a href="https://doc.tmssoftware.com/biz/sparkle/guide/hosting.html#workers">background workers</a>, starts them in order and stops them in reverse. <code>ServiceOptions</code> is the name, display name and description the service gets when you install it. And if something fails to start - a port already taken, a database that isn&#39;t there - whatever is running is stopped, the error is logged, and the process exits with code 1, so the failure is visible to systemd, to the Service Control Manager and to your deployment script instead of looking like a normal shutdown.</p>

<p>A worker is that job runner or scheduler: a class with a loop of its own. Derive from <a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Host.Contracts/THostedWorker/index.html"><code>THostedWorker</code></a>, override <code>Execute</code>, and the host gives it a thread:</p>

<pre><code>type
  TNightlyJobsWorker = class(THostedWorker)
  protected
    procedure Execute(const AStop: IStopToken); override;
  end;

procedure TNightlyJobsWorker.Execute(const AStop: IStopToken);
begin
  repeat
    RunPendingJobs(AStop);
  until AStop.WaitFor(60000);
end;
</code></pre>

<p>The loop waits on the <a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Host.Contracts/IStopToken/index.html"><code>IStopToken</code></a> instead of calling <code>Sleep</code>, and that&#39;s what makes stopping instant: a service stop, a <code>systemctl stop</code> or a Ctrl+C ends the wait immediately, wherever in the minute it happened to be. Pass the token into long-running work and it can give up halfway instead of holding up the shutdown.</p>

<p>Still one project, still one executable.</p><p><br></p>

<h3>Already have a server? It&#39;s a small change</h3>

<p>Existing projects keep working: the previous wizards are still in the gallery under <strong>TMS BIZ (Deprecated)</strong>, and the <code>Sparkle.App</code> units still compile.</p>

<p>But moving to the host is usually a few lines. A server generated by the old wizards, for instance, has a <code>Server</code> unit with <code>StartServer</code> and <code>StopServer</code> procedures shared by its VCL, service and console projects. Keep that unit exactly as it is, give it a one-file console project, and the other projects are no longer needed:</p>

<pre><code>uses
  Sparkle.Host,
  Sparkle.Host.Shutdown,
  Server in &#39;Server.pas&#39;;

begin
  RunHost(
    procedure
    begin
      StartServer;
      try
        WaitForShutdown;
      finally
        StopServer;
      end;
    end);
end.
</code></pre>

<p>Your server code doesn&#39;t change. <code>WaitForShutdown</code> simply replaces the <code>ReadLn</code> you had in the console version - and that single line is what tells the host the server is up, which is what a Windows service has to report to the Service Control Manager and what systemd wants to know.</p>

<p>The <a href="https://doc.tmssoftware.com/biz/sparkle/guide/hosting.html#migrating-existing-servers">migration guide</a> covers this and the other starting points, including hand-written <code>TService</code> projects.</p><p><br></p>

<h3>Conclusion</h3>

<p>Deployment is the part of a server project that has the least to do with your application and causes the most trouble. The new hosting layer takes it off your hands:</p>

<ul>
  <li><strong>One project and one executable</strong> for development, Windows services and Linux daemons.</li>
  <li><strong>F9 to run, Ctrl+C to stop</strong>, with the same code and the same behavior you ship.</li>
  <li><strong>Deployment built into the binary</strong>: <code>--install</code> on Windows, <code>--systemd-unit</code> on Linux.</li>
  <li><strong>Correct service behavior for free</strong> - graceful shutdown, start and stop reporting, meaningful exit codes.</li>
  <li><strong>Wizards for Sparkle, XData and RemoteDB</strong> that create this shape of project for you.</li>
  <li><strong>Existing servers keep working</strong>, and move over with almost no code change.</li>
</ul>

<p>A complete example - an HTTP server and a background worker in one process - ships in the Sparkle distribution under <code>demos/host</code>.</p>

<p>To learn more, read the <strong><a href="https://doc.tmssoftware.com/biz/sparkle/guide/hosting.html">Hosting chapter</a></strong> in the Sparkle documentation, the getting-started guides for the <strong><a href="https://doc.tmssoftware.com/biz/xdata/guide/start.html#creating-the-server-using-the-xdata-server-wizards">XData Server wizard</a></strong> and the <strong><a href="https://doc.tmssoftware.com/biz/remotedb/guide/server.html#remotedb-server-wizard">RemoteDB Server wizard</a></strong>, and the <strong><a href="https://doc.tmssoftware.com/biz/sparkle/about/whats-new.html">what&#39;s new list</a></strong>. And visit the product pages of <strong><a href="https://www.tmssoftware.com/site/sparkle.asp">TMS Sparkle</a></strong>, <strong><a href="https://www.tmssoftware.com/site/xdata.asp">TMS XData</a></strong> and <strong><a href="https://www.tmssoftware.com/site/remotedb.asp">TMS RemoteDB</a></strong>, all part of <strong><a href="https://www.tmssoftware.com/site/tmsbizintro.asp">TMS BIZ</a></strong>.</p>

		
		
		
		
		
		
		<br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿Building a PKCS#12/PFX Exporter with TMS Crypto Pack: Inside TX509Certificate]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2525</link>
		<author>Bernard Roussely</author>
		<pubDate>Wed, 23 Sep 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		<h5 dir="ltr">TMS Cryptography Pack proposes functions/procedures to generate and decode many PKIX structures using the "PEM" format. That includes certificates, keys and signatures files. It also supported the decoding of PFX certificates (with either&nbsp;<code>keyBag</code>,<code>certBag</code>, or&nbsp;<code>pkcs8ShroudedKeyBag</code>) and as of 5.3.0.0, it supports the generation of PFX for "cert bags" and "key bags".</h5><p>This post explains how the internal of this feature works.</p><h3 dir="ltr">Why PKCS#12 is trickier than it looks</h3>
<h5 dir="ltr">A <code>.pfx</code>/<code>.p12</code> file looks like a single opaque blob, but it&#39;s a small tower of nested ASN.1 structures, each with its own encoding rules, several of which are easy to get subtly wrong without any error message at write time &#151; the file parses fine, opens in some tools, and then fails silently in others (wrong MAC, unreadable key, orphaned certificate). <code>TX509Certificate</code> implements this format from scratch in Delphi, building and parsing the DER by hand rather than relying on an external ASN.1 library. This article walks through what the format actually requires, how the class implements each layer, and how to cross-check the output against OpenSSL at every step.</h5>
<h3 dir="ltr">The structure, top to bottom</h3>
<div><div role="group" aria-label="Code" tabindex="0"><div><div></div></div><div><pre style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono);"><code style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono); white-space: pre-wrap;">PFX ::= SEQUENCE {
    version     INTEGER {v3(3)},
    authSafe    ContentInfo,
    macData     MacData OPTIONAL
}</code></pre></div></div></div>
<p dir="ltr"><code>authSafe</code> is a <code>ContentInfo</code> of type <code>id-data</code>, whose payload is itself an <code>AuthenticatedSafe ::= SEQUENCE OF ContentInfo</code>. Each element of that sequence carries one "safe" &#151; a certificate, a key, or a group of either &#151; wrapped either in plain <code>id-data</code> or in <code>encryptedData</code> when confidentiality is wanted for that particular safe. Inside each safe sits a <code>SafeContents ::= SEQUENCE OF SafeBag</code>, and each <code>SafeBag</code> carries a typed payload (<code>keyBag</code>,<code>certBag</code>, <code>pkcs8ShroudedKeyBag</code>,&nbsp;<code>CRLBag</code>,&nbsp;<code>SecretBag</code>, and&nbsp;<code>SafeContents</code>) plus an optional set of attributes (<code>localKeyID</code>, <code>friendlyName</code>) used to associate a certificate with its private key.</p>
<p dir="ltr">That&#39;s four or five levels of nesting before you reach a single useful byte, and every level has its own tag, its own length, and its own rules about implicit versus explicit tagging. Get one length wrong, or wrap something in <code>[0] IMPLICIT</code> where <code>[0] EXPLICIT</code> was required, and the file becomes silently unreadable to strict parsers while looking fine in lenient ones &#151; a genuinely common failure mode when building this by hand.</p>
<h3 dir="ltr">New building blocks in <code>TX509Certificate</code></h3>
<h4 dir="ltr"><code>BuildPKCS8PrivateKeyInfo</code></h4>
<p dir="ltr">Wraps the raw key material (RSA or EC) into a standard PKCS#8 <code>PrivateKeyInfo</code>:</p>
<div><div role="group" aria-label="Code" tabindex="0"><div><div></div></div><div><pre style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono);"><code style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono); white-space: pre-wrap;">PrivateKeyInfo ::= SEQUENCE {
    version                   INTEGER (0),
    privateKeyAlgorithm       AlgorithmIdentifier,
    privateKey                OCTET STRING
}</code></pre></div></div></div>
<p dir="ltr">For RSA, <code>privateKeyAlgorithm</code> carries <code>rsaEncryption</code> (<code>1.2.840.113549.1.1.1</code>) with <code>NULL</code> parameters. For EC, it carries <code>id-ecPublicKey</code> (<code>1.2.840.10045.2.1</code>) with the named curve&#39;s OID as parameters instead of <code>NULL</code> &#151; a detail that&#39;s easy to copy-paste wrong from the RSA branch, since both look identical except for that one field.</p>
<p dir="ltr"><strong>OpenSSL equivalent</strong> &#151; inspecting an unencrypted PKCS#8 key:</p>
<div><div role="group" aria-label="Code bash" tabindex="0"><div><div></div></div><div><pre style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono);"><code class="language-bash" style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono); white-space: pre;"><span><span><span style="color: rgb(0, 81, 194);">openssl</span> <span style="color: rgb(0, 128, 0);">pkey</span> <span style="color: rgb(0, 128, 0);">-in</span> <span style="color: rgb(0, 128, 0);">key.pem</span> <span style="color: rgb(0, 128, 0);">-text</span> <span style="color: rgb(0, 128, 0);">-noout</span></span></span></code></pre></div></div></div>
<h4 dir="ltr"><code>BuildPkcs8ShroudedKeyBag</code></h4>
<p dir="ltr">Encrypts the <code>PrivateKeyInfo</code> with PBES2 (RFC 8018): PBKDF2-HMAC-SHA256 to derive the key, AES-256-CBC to encrypt it. The resulting <code>AlgorithmIdentifier</code> looks like:</p>
<div><div role="group" aria-label="Code" tabindex="0"><div><div></div></div><div><pre style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono);"><code style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono); white-space: pre-wrap;">PBES2 { PBKDF2 { salt, iterations, keyLength, prf }, AES-256-CBC { iv } }</code></pre></div></div></div>
<p dir="ltr"><code>keyLength</code> here is in <strong>bytes</strong>, not bits.</p>
<p dir="ltr"><strong>OpenSSL equivalent</strong> &#151; creating a PBES2/AES-256/PBKDF2-SHA256 encrypted PKCS#8 key directly:</p>
<div><div role="group" aria-label="Code bash" tabindex="0"><div><div></div></div><div><pre style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono);"><code class="language-bash" style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono); white-space: pre;"><span><span><span style="color: rgb(0, 81, 194);">openssl</span> <span style="color: rgb(0, 128, 0);">pkcs8</span> <span style="color: rgb(0, 128, 0);">-topk8</span> <span style="color: rgb(0, 128, 0);">-v2</span> <span style="color: rgb(0, 128, 0);">aes-256-cbc</span> <span style="color: rgb(0, 128, 0);">-v2prf</span> <span style="color: rgb(0, 128, 0);">hmacWithSHA256</span></span><span>  <span style="color: rgb(0, 128, 0);">-in</span> <span style="color: rgb(0, 128, 0);">key.pem</span> <span style="color: rgb(0, 128, 0);">-out</span> <span style="color: rgb(0, 128, 0);">key_encrypted.pem</span></span></span></code></pre></div></div></div>
<h4 dir="ltr"><code>BuildCertBag</code></h4>
<p dir="ltr">For a certificate stored <strong>unencrypted</strong>, the <code>SafeBag</code> points directly at type <code>x509Certificate</code> (<code>1.2.840.113549.1.9.22.1</code>):</p>
<div><div role="group" aria-label="Code" tabindex="0"><div><div></div></div><div><pre style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono);"><code style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono); white-space: pre-wrap;">SafeBag { bagId=x509Certificate, bagValue=[0]{OCTET STRING cert}, bagAttributes }</code></pre></div></div></div>
<h4 dir="ltr"><code>BuildBagForEncryption</code> + <code>BuildEncryptedContentInfo</code></h4>
<p dir="ltr">For a certificate stored <strong>encrypted</strong>, OpenSSL &#151; and this class, once corrected to match it &#151; uses a different, doubly-nested <code>bagId</code>:</p>
<div><div role="group" aria-label="Code" tabindex="0"><div><div></div></div><div><pre style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono);"><code style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono); white-space: pre-wrap;">SafeBag {
  bagId = certBag (1.2.840.113549.1.12.10.1.3),
  bagValue = [0] EXPLICIT SEQUENCE {
      certType  = x509Certificate,
      certValue = [0] EXPLICIT OCTET STRING (the cert DER)
  },
  bagAttributes
}</code></pre></div></div></div>
<p dir="ltr">This whole <code>SafeBag</code> &#151; not just the certificate &#151; is what gets encrypted, as the plaintext of an <code>EncryptedContentInfo</code> sitting inside a <code>ContentInfo</code> of type <code>encryptedData</code> (<code>1.2.840.113549.1.7.6</code>) at the <code>AuthenticatedSafe</code> level:</p>
<div><div role="group" aria-label="Code" tabindex="0"><div><div></div></div><div><pre style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono);"><code style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono); white-space: pre-wrap;">ContentInfo {
  contentType = encryptedData,
  content = [0] EXPLICIT EncryptedData {
      version = 0,
      encryptedContentInfo = SEQUENCE {
          contentType = id-data,
          contentEncryptionAlgorithm = PBES2 { ... same shape as the key ... },
          encryptedContent = [0] IMPLICIT OCTET STRING
      }
  }
}</code></pre></div></div></div>
<p dir="ltr">Two details worth calling out because they don&#39;t announce themselves as bugs &#151; decryption just silently produces garbage:</p>
<ul dir="ltr"><li><code>encryptedContent</code> must be tagged <code>[0] IMPLICIT</code> (<code>0x80</code>, a primitive context tag directly in front of the ciphertext bytes), <strong>not</strong> <code>[0] EXPLICIT</code> (<code>0xA0</code>) wrapping an inner <code>OCTET STRING</code>. Both parse as valid DER, so a parser can misread one for the other without erroring &#151; the mismatch only surfaces if you try to decrypt and compare byte-for-byte against a reference.</li><li>The <code>bagValue</code> of the certBag-wrapping <code>SafeBag</code>, and the inner <code>certValue</code>, both need their own <code>[0] EXPLICIT</code> wrapper &#151; easy to drop one level when hand-assembling nested strings.</li></ul>
<p dir="ltr"><strong>OpenSSL equivalent</strong> &#151; producing a PFX with the certificate encrypted (in addition to the key), with TMS CP supported algorithms:</p>
<div><div role="group" aria-label="Code bash" tabindex="0"><div><div></div></div><div><pre style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono);"><code class="language-bash" style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono); white-space: pre;"><span><span><span style="color: rgb(0, 81, 194);">openssl</span> <span style="color: rgb(0, 128, 0);">pkcs12</span> <span style="color: rgb(0, 128, 0);">-export</span> <span style="color: rgb(0, 128, 0);">-in</span> <span style="color: rgb(0, 128, 0);">cert.pem</span> <span style="color: rgb(0, 128, 0);">-inkey</span> <span style="color: rgb(0, 128, 0);">key.pem</span></span><span>  <span style="color: rgb(0, 128, 0);">-out</span> <span style="color: rgb(0, 128, 0);">out.pfx</span> <span style="color: rgb(0, 128, 0);">-certpbe</span> <span style="color: rgb(0, 128, 0);">AES-256-CBC</span> <span style="color: rgb(0, 128, 0);">-keypbe</span> <span style="color: rgb(0, 128, 0);">AES-256-CBC</span></span><span>  <span style="color: rgb(0, 128, 0);">-macalg</span> <span style="color: rgb(0, 128, 0);">sha256</span></span></span></code></pre></div></div></div>
<p dir="ltr">(Without <code>-certpbe</code>, recent OpenSSL defaults to leaving the certificate in plain <code>id-data</code> &#151; which is also perfectly valid PKCS#12, and the simpler case this class supports as well.)</p>
<h4 dir="ltr"><code>BuildMacData</code></h4>
<p dir="ltr">The <code>MacData</code> structure authenticates the entire <code>AuthenticatedSafe</code>:</p>
<div><div role="group" aria-label="Code" tabindex="0"><div><div></div></div><div><pre style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono);"><code style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono); white-space: pre-wrap;">MacData ::= SEQUENCE {
    mac         DigestInfo { algorithm, digest },
    macSalt     OCTET STRING,
    iterations  INTEGER DEFAULT 1
}</code></pre></div></div></div>
<p dir="ltr">The key used for this HMAC is <strong>not</strong> derived with PBKDF2. RFC 7292&#39;s Annex B defines a separate, older key-derivation function specific to PKCS#12: the password is encoded as a BMPString (UTF-16BE, null-terminated), repeated together with the salt to fill whole hash-block-sized buffers, and the hash function is applied iteratively to an evolving buffer rather than through the standard PBKDF2 construction. Reusing the PBES2/PBKDF2 routine here &#151; an easy mistake, since both derive "a key from a password and a salt" &#151; produces a structurally perfect file with a MAC that never validates.</p><p dir="ltr"><strong>OpenSSL equivalent</strong> &#151; verifying a file&#39;s MAC and listing its contents:</p>
<div><div role="group" aria-label="Code bash" tabindex="0"><div><div></div></div><div><pre style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono);"><code class="language-bash" style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono); white-space: pre;"><span><span><span style="color: rgb(0, 81, 194);">openssl</span> <span style="color: rgb(0, 128, 0);">pkcs12</span> <span style="color: rgb(0, 128, 0);">-info</span> <span style="color: rgb(0, 128, 0);">-in</span> <span style="color: rgb(0, 128, 0);">file.pfx</span> <span style="color: rgb(0, 128, 0);">-noout</span> <span style="color: rgb(0, 128, 0);">-passin</span> <span style="color: rgb(0, 128, 0);">pass:yourpassword </span></span></span></code># pick a strong password :-)</pre></div></div></div>
<p dir="ltr">A successful run without a MAC-mismatch error is the fastest real-world confirmation that both the <code>AuthenticatedSafe</code> structure and the HMAC are correct together &#151; cheaper than manual ASN.1 inspection for a quick sanity check, though it won&#39;t tell you <em>why</em> something is wrong if it fails. You can still use the <a href="https://lapo.it/asn1js/" target="_blank">https://lapo.it/asn1js/</a>&nbsp;decoder to visualize and check all ASN.1 structures.</p>
<h3 dir="ltr">Putting it together revisited: <code>ExportToPFX</code></h3>
<div><div role="group" aria-label="Code pascal" tabindex="0"><div><div></div></div><div>Up to version 5.2.x.y, ExportToPFX was a call to OpenSSL with a series of parameters. The new code looks like this:</div><div><pre style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono);"><code class="language-pascal" style="color: rgb(20, 24, 31); background: transparent; font-family: var(--font-mono); white-space: pre;"><span><span>PKCS8Key := BuildPKCS8PrivateKeyInfo;
</span><span>InitPfxParameters(Password);
</span><span>FPfx.EncryptionKey := ASN1.GenerateKDFKey(FPfx.Password, FPfx.KdfSalt,
</span><span>                                           FPfx.KdfIterations, FPfx.KdfOutputSize * <span style="color: rgb(0, 128, 128);">8</span>);
</span><span>FPfx.EncryptedKeyset := ASN1.EncryptPrivateKeyInfo(FPfx.EncryptionKey, FPfx.EncryptionIV, PKCS8Key);
</span><span>
</span><span>CertBag := BuildCertBag(FCrtRaw);                     <span style="color: rgb(110, 118, 135);">// or BuildBagForEncryption, for the encrypted path</span>
</span><span>KeyBag  := BuildPkcs8ShroudedKeyBag(FPfx.EncryptedKeyset);
</span><span>
</span><span>SafeContents := BuildSafeContents(CertBag, KeyBag);    <span style="color: rgb(110, 118, 135);">// = the AuthenticatedSafe itself</span>
</span><span>AuthSafe     := BuildContentInfoData(SafeContents);    <span style="color: rgb(110, 118, 135);">// outer ContentInfo wrapper</span>
</span><span>MacData      := BuildMacData(SafeContents);            <span style="color: rgb(110, 118, 135);">// MAC over the AuthenticatedSafe, not SafeContents&#39; inner bags</span>
</span><span>
</span><span>PFX := BuildPfxRoot(AuthSafe, MacData);</span></span></code></pre></div></div></div>
<p dir="ltr">The naming here is a little misleading on purpose: <code>SafeContents</code> is where the two per-bag <code>ContentInfo</code> elements are actually assembled, making it the true <code>AuthenticatedSafe</code>; <code>AuthSafe</code> is one further wrapper around that, the outer <code>ContentInfo</code> that becomes <code>PFX.authSafe</code>. Keeping straight <em>which</em> buffer the MAC is computed over &#151; the <code>AuthenticatedSafe</code>, always including its own <code>SEQUENCE</code> tag and length, never the bare concatenation of bags inside it, and never the doubly-wrapped <code>AuthSafe</code> &#151; is the single detail most worth getting right first, since every other structural mistake tends to surface as a parse error, while this one surfaces only as a MAC that mysteriously never matches.</p>
<h3 dir="ltr">Testing checklist</h3>
<p dir="ltr">For anyone extending (not all OIDs are present) or auditing code like this, the fastest feedback loop is:</p>
<ol dir="ltr"><li><strong>Structural check</strong> &#151; <code>openssl asn1parse -inform DER -in file.pfx -i</code>, or <code>-strparse &lt;offset&gt;</code> to descend past an outer <code>OCTET STRING</code> that the tool won&#39;t auto-recurse into.</li><li><strong>Integrity check</strong> &#151; <code>openssl pkcs12 -info -in file.pfx -noout -passin pass:...</code>; a MAC failure here means the buffer hashed at export didn&#39;t match the buffer written.</li><li><strong>Round-trip check</strong> &#151; decrypt and re-import with a second, independent implementation (even a short Python script using <code>cryptography</code> and <code>hashlib.pbkdf2_hmac</code>) rather than trusting your own decoder, since a decoder written by the same hand as the encoder will happily agree with its own mistakes.</li></ol><p>More OpenSSL examples are provided in the <i><b>\Demo\VCL\PFX</b> folder</i>. Several PFX certificates to test the decode function can be found in the <i><b>\Demo\VCL\PFX\Certs</b></i> folder.&nbsp;</p><p>The VCL Demo has been updated to generate standard X509 certificates and PFX certificates, for which a password is required.</p>
		<br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿Hawk One: a voxel helicopter game written in Delphi with TMS WEB Core]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2524</link>
		<author>Aäron Decramer</author>
		<pubDate>Wed, 23 Sep 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		
		<p></p><p><span style="background-color: rgba(0, 158, 227); color: white; padding: 4px 8px; text-align: center; border-radius: 5px; font-size: 1.5em;">Customer Case</span></p>
		
<div><img src="https://www.tmssoftware.com/site/img/blog/Hawk_One.png" style="" alt="TMS Software Delphi  Components "><br></div><div><br></div>

<h1>Hawk One: a voxel helicopter game written in Delphi with TMS WEB Core</h1>

<p>In the 1990s, <b>Ralf K&#228;hlke </b>spent a lot of time playing Comanche, NovaLogic&#39;s helicopter game with its voxel landscapes. What stuck with him was flying low over the mountains and valleys, using hills as cover. Somewhere along the way he added an item to his personal to-do list: build a helicopter game like that himself.</p>

<p>More than thirty years later, he did. <a href="https://stainlessfame.itch.io/hawkone" target="_blank">Hawk One</a> is now playable on itch.io, it runs in the browser, and it is written in Delphi and compiled to JavaScript with <a href="https://www.tmssoftware.com/site/tmswebcore.asp" target="_blank">TMS WEB Core</a>.</p>

<p style="text-align: center; margin: 32px 0;">
  <a href="https://stainlessfame.itch.io/hawkone" target="_blank" rel="noopener" style="display: inline-block; padding: 14px 32px; background-color: #000000; color: #ffffff; font-family: Arial, Helvetica, sans-serif; font-size: 17px; font-weight: bold; text-decoration: none; border-radius: 0; border-bottom: 4px solid #00A3E4;">
    Play Hawk One
  </a>
</p>
<br>

<h2>It started with a renderer</h2>

<p>Hawk One wasn&#39;t a game at first. Ralf wanted to find out whether he could write his own voxel-style terrain renderer and get it fast enough to run in a web browser. There is no Unity or Unreal underneath. The renderer, flight model, HUD and game logic are his own Pascal code.</p>

<p>The first versions ran at 9 frames per second. A lot of the work after that went into keeping calculations out of the render loop and precomputing terrain data, until the frame rate was acceptable on a desktop PC and later on phones and tablets too.</p>

<p>Once he could fly over his own landscape, the project grew into a game almost by itself. Helicopter movement came first, then a HUD, a minimap, radar contacts and a compass. After that came weapons: a gun plus air-to-ground and air-to-air missiles, with target locking, ammunition and scoring.</p>

<figure style="margin: 24px 0; text-align: center;">
  <img src="https://www.tmssoftware.com/site/img/blog/N9jIeQ.jpeg" alt="Hawk One gameplay on a phone: voxel terrain with a river, compass heading tape, minimap, mission timer and an air-to-ground missile lock on a target" style="max-width: 100%; height: auto;">
  <figcaption style="font-size: 14px; color: #555555; margin-top: 8px;">Locking an air-to-ground missile onto a target, with the touch controls visible on the left and right.</figcaption>
</figure>

<p>The current mission gives you five minutes to find and destroy every enemy radar contact. A radio officer briefs you before takeoff and has something different to say afterwards depending on how it went.</p>

<figure style="margin: 24px 0; text-align: center;">
  <img src="https://www.tmssoftware.com/site/img/blog/9XsiJS.png" alt="Pixel-art radio officer with a headset next to the mission briefing text" style="max-width: 100%; height: auto;">
  <figcaption style="font-size: 14px; color: #555555; margin-top: 8px;">The mission briefing from base.<br><br></figcaption>
</figure>

<h2>Keyboard, gamepad and touch</h2>

<p>Input turned out to be harder than Ralf expected. Controls that feel right on a keyboard don&#39;t work on a controller, and neither works on a phone. Hawk One now switches between them automatically. On a desktop you can use the keyboard or plug in a gamepad, which also works in the menus. On a phone or tablet, two virtual joysticks and touch buttons appear on screen. There are two flight models as well. Arcade is the default, so you can start flying without reading a manual, and a more simulation-oriented option is there for people who want it.</p>

<figure style="margin: 24px 0; text-align: center;">
  <img src="https://www.tmssoftware.com/site/img/blog/mi5uSQ.png" alt="Control settings screen with input device options (auto, keyboard, touch, joystick/gamepad) and flight control model options (arcade, simulator)" style="max-width: 100%; height: auto;">
  <figcaption style="font-size: 14px; color: #555555; margin-top: 8px;">Input device and flight model can also be set by hand.</figcaption>
</figure>

<p>The game can also be installed as a Progressive Web App and launched from the home screen, so the same build runs on a Windows PC, a tablet and a phone.<br><br></p>

<h2>Why we like this project</h2>

<p>Most <a href="https://www.tmssoftware.com/site/tmswebcore.asp" target="_blank">TMS WEB Core</a> applications we see are business software: dashboards, forms, portals, back-office tools. Hawk One is a different kind of workload. A real-time renderer redraws the whole screen every frame and has to react to input with no noticeable lag, and here all of that code is written in Object Pascal.</p>

<p>Ralf&#39;s day job is on the business side. He runs his own company, <a href="https://sqlmanagement.de" target="_blank" rel="noopener">SQLmanagement K&#228;hlke</a>, and for nearly 30 years he has been developing and licensing HK/2, a software package for sub-metering and heating cost billing. HK/2 is built with a range of TMS components, including <a href="https://www.tmssoftware.com/site/tmswebcore.asp" target="_blank">TMS WEB Core</a> and <a href="https://www.tmssoftware.com/site/xdata.asp" target="_blank">TMS XData</a>. Hawk One is a side project, written with the same language and much of the same tooling he uses for his commercial software. It shows that <a href="https://www.tmssoftware.com/site/tmswebcore.asp" target="_blank">TMS WEB Core</a> can handle performance-sensitive code in the browser, and that Delphi developers can bring their existing skills to a platform that runs on nearly any device.</p>

<p>It&#39;s also a lot of fun to play.<br><br></p>

<h2>An early version, and he wants feedback</h2>

<p>Hawk One is in active development. Ralf plans more missions, enemies that fight back, more varied terrain, better effects and more sound. He&#39;s asking players how the controls feel, how the game performs on their device, and whether the five-minute mission is too easy or too hard. If you try it, leave a comment on the <a href="https://stainlessfame.itch.io/hawkone" target="_blank" rel="noopener">itch.io page</a>.<br><br></p>

<p>Have you built something unusual with <a href="https://www.tmssoftware.com/site/tmswebcore.asp" target="_blank">TMS WEB Core</a>? We&#39;d like to hear about it.</p><br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿Delphi 13.2 support is here]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2523</link>
		<author>Aäron Decramer</author>
		<pubDate>Wed, 23 Sep 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		
		<div><img src="https://www.tmssoftware.com/site/img/blog/Delphi_13.2_Support_Banner_1280x300.png" style="" alt="TMS Software Delphi  Components "><br></div><div><br></div><div><div>Every actively maintained TMS product now supports Delphi 13.2.</div><div><br></div><div>Most of our products already worked with Delphi 13.2 on the day it came out. TMS WEB Core needed an update first, and that update is now available as v3.0.4.0. It works with all Delphi 13.x releases, so you can use it on 13.0, 13.1 and 13.2. With this last piece in place, you can move your projects to Delphi 13.2 without waiting on any of our products.</div><div><br></div><div>You can get TMS WEB Core v3.0.4.0 and the latest versions of all other actively maintained TMS products through <a href="https://doc.tmssoftware.com/smartsetup/" target="_blank">TMS Smart Setup</a>.</div><div><br></div><div>Should you run into a problem with a TMS product on Delphi 13.2, let us know on the <a href="https://www.tmssoftware.com/site/support.asp" target="_blank">support center</a> or through our <a href="https://www.tmssoftware.com/site/support_mail.asp" target="_blank">email support</a>.</div></div>
		
		
		
		
		
		<br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿Back to School: TMS Academic Products Now Support Delphi 13 Community Edition]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2522</link>
		<author>Aäron Decramer</author>
		<pubDate>Tue, 22 Sep 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		
		
		<div><img src="https://www.tmssoftware.com/site/img/blog/TMS_Academic_Back_to_School_Banner.png" style="" alt="TMS Software Delphi  Components "><br></div><div><br></div><div>Summer break is over, and if you&#39;re a student or teacher picking Delphi back up for the new school year, we have good news: every product in the <a href="https://www.tmssoftware.com/site/academic.asp" target="_blank">TMS Academic program</a> now works with <a href="https://www.embarcadero.com/products/delphi/starter" target="_blank">Delphi 13 Community Edition</a>.</div><div><br></div><div>That matters because Delphi 13 is the version most students will actually have installed. <a href="https://www.embarcadero.com/products/delphi/starter" target="_blank">Embarcadero&#39;s Community Edition</a> is free, and it&#39;s usually the first (and sometimes only) copy of Delphi that lands on a student laptop. Up to now, keeping our academic licenses in step with each new Community Edition release has been an ongoing job on our side. With Delphi 13, that work is done, and it applies across the board rather than to a single product.</div><div><br></div><div><br></div><h4>What&#39;s included</h4><div>The TMS Academic program gives students and teachers full, free versions of these products for non-commercial use:</div><ul><li><a href="https://www.tmssoftware.com/site/tmsaistudio.asp" target="_blank">TMS AI Studio</a></li><li><a href="https://www.tmssoftware.com/site/tmsvcluipack.asp" target="_blank">TMS VCL UI Pack</a></li><li><a href="https://www.tmssoftware.com/site/tmsfncuipack.asp" target="_blank">TMS FNC UI Pack</a></li><li><a href="https://www.tmssoftware.com/site/tmswebcore.asp" target="_blank">TMS WEB Core</a></li><li><a href="https://www.tmssoftware.com/site/flexcel.asp" target="_blank">TMS FlexCel for VCL &amp; FMX</a></li><li><a href="https://www.tmssoftware.com/site/aurelius.asp" target="_blank">TMS Aurelius</a></li><li><a href="https://www.tmssoftware.com/site/tmsanalytics.asp" target="_blank">TMS Analytics &amp; Physics Pack</a></li><li><a href="https://www.tmssoftware.com/site/tmsasync.asp" target="_blank">TMS Async</a></li><li><a href="https://www.tmssoftware.com/site/tmsmqtt.asp" target="_blank">TMS MQTT</a></li></ul><div><br></div><div>Each one comes with a full year of license and updates, and you get an account on our website with access to our support center, so if you get stuck on an assignment or a side project, help is one ticket away.</div><div><br></div><div><br></div><h4>Why this is worth trying this semester</h4><div>If you&#39;re building your first data-driven Windows app, a grid or planner from the VCL UI Pack will save you weeks of layout work. If your course touches web development, TMS WEB Core lets you write actual web applications in Delphi instead of switching languages halfway through a project. And if your final-year project involves anything AI-related, TMS AI Studio is worth a look: it&#39;s built for adding AI features to Delphi and C++Builder apps without having to become a machine learning specialist first.</div><div><br></div><div>None of this requires a purchase order or a departmental budget approval. You register with your school or university email address, and the credentials for downloading the installer land in your inbox.</div><div><br></div><h4>How to get started</h4><ol><li>Go to the <a href="https://www.tmssoftware.com/site/academic.asp" target="_blank">TMS Academic page</a></li><li>Register with your academic email address</li><li>Log in and download the installer for the product (or products) you need</li><li><a href="https://doc.tmssoftware.com/smartsetup/">Download TMS Smart Setup</a>, our command-line and GUI installer, and log in with the credentials you received</li><li>Install the product (or products)</li></ol><div><br></div><div>The license lasts one year and can be renewed as long as you&#39;re still enrolled as a student or working as a teacher. And if a personal project ever turns into something commercial, switching to the full licensed product later doesn&#39;t require changing anything in your code.</div>
		
		
		
		
		
		
		<br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿TMS FixInsight reaches End of Life]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2520</link>
		<author>Dennis Röhner</author>
		<pubDate>Tue, 22 Sep 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		<div>We would like to inform our customers that <a href="https://www.tmssoftware.com/site/fixinsight.asp" target="_blank">TMS FixInsight</a> has reached End of Life (EOL) and will be discontinued as a TMS Software product. FixInsight has been a valuable tool for Delphi developers, helping identify potential bugs, code smells and other issues directly in Delphi projects. However, we are no longer in a position to provide the ongoing development and maintenance that we expect from an actively supported TMS product. For this reason, sales of TMS FixInsight will end effective immediately.</div><div><br></div><div>Existing FixInsight licenses remain valid. Customers will continue to have access to the versions of FixInsight they are currently entitled to through their TMS Software account. The current version of FixInsight will remain the final release. No further maintenance releases, new features or compatibility updates for future Delphi versions are planned.</div><div><br></div><div>We will continue to assist existing customers with licensing, installation and other questions where possible. However, as the product is no longer under active maintenance, we cannot guarantee fixes for technical issues or compatibility with future versions of Delphi. If you are currently using FixInsight successfully in your development environment, you can of course continue to do so.</div><div><br></div><div>We would like to thank everyone who has used FixInsight over the years and made it part of their Delphi development workflow.</div>
		
		
		
		
		<br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿TMS FNC Localization is now in open beta]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2521</link>
		<author>Aäron Decramer</author>
		<pubDate>Mon, 21 Sep 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		
		<div><img src="https://www.tmssoftware.com/site/img/blog/tmsfnclocalizationblog.png" style="width: 33%; float: right;" class="note-float-right" alt="TMS Software Delphi  Components tmsfnclocalization"></div><div><div>On September 3, we announced the <a href="https://www.tmssoftware.com/site/blog.asp?post=2513" target="_blank">closed beta of TMS FNC Localization</a>. Today we&#39;re opening it up: <b>TMS FNC Localization</b> is now available as an <b>open beta</b>, and anyone can try it.</div><div><br></div><div>You can download the trial version from the <a href="https://www.tmssoftware.com/site/tmsfnclocalization.asp" target="_blank">TMS FNC Localization product page</a>. If you have a TMS ALL-ACCESS license, you can get the beta through TMS Smart Setup instead.</div><div><br></div><div><br></div></div><div><h4>From a real-world need to TMS FNC Localization</h4><div>Readers who follow our blog may already recognize the name <b>Erwin Denissen</b>.</div><div><br></div><div>Erwin is the developer behind<b> FontCreator from High-Logic</b>, which we featured earlier in <a href="https://www.tmssoftware.com/site/blog.asp?post=1292" target="_blank">one of our customer showcase blog posts</a>. While working on his own software, he ran into the same problems that come with adding and maintaining localization in an application.</div><div><br></div><div>He couldn&#39;t find a tool that matched the workflow he had in mind, so he built his own.</div><div><br></div><div>That personal project eventually brought Erwin and TMS Software together. We saw potential in what he&#39;d built, and out of that collaboration came the product we&#39;re presenting here: <a href="https://www.tmssoftware.com/site/tmsfnclocalization.asp" target="_blank"><b>TMS FNC Localization</b></a>.</div><div><br></div><div>Thanks again to Erwin for the work, ideas and experience he&#39;s put into making this possible.</div><div><br></div><div><br></div></div><div><div><h4>Collect, Translate, Apply!</h4><div><div>TMS FNC Localization is built around three steps: collect the texts, translate them, and apply the selected language. Rather than building your own localization infrastructure, you get dedicated components for each step.</div><div><br></div><div><br></div></div><div><b>Collect</b></div><div><b><br></b></div><div>Finding everything that needs translating is often the hardest part, especially in an existing application. <b>TTMSFNCLocalizationCollector</b> gathers localizable content from your application so you don&#39;t have to track down every caption and text property by hand.</div><div></div><pre class="delphi" name="code">//Analyze and collect the application - only applies to live forms
TMSFNCLocalizationCollector.AnalyzeApplication;
//Analyze and collect a given form
TMSFNCLocalizationCollector.AnalyzeForm(Self);</pre><div><img src="https://www.tmssoftware.com/site/img/blog/tmsfnclocalizationblog1.png" style="width: 100%;" alt="TMS Software Delphi  Components tmsfnclocalization"><br></div><div><br></div><div><b>Translate<br><br></b></div><div><div style="">Once the strings are collected, they need to be managed and translated.</div><div style=""><br></div><div style=""><b>TTMSFNCLocalizationEditor</b> gives you a way to work with the collected texts and their translations. Open the editor at runtime and you can see translation changes reflected immediately in your running application.<br><br></div></div><div><div><pre class="delphi" name="code">TMSFNCLocalizationEditor.Execute;</pre></div><div><br><div>You can also open it at design time, from the context menu of TTMSFNCLocalizationCollector or any TForm. When you trigger the editor at design time, it will also show you which forms haven&#39;t been discovered yet.</div><div><br></div><div>Don&#39;t want to translate every item by hand? You can use optional AI assistance to generate translations and just verify the results. We&#39;ve added support for several AI services through your own API key, including <b>OpenAI</b>, <b>Claude</b>, <b>Gemini</b>, <b>Grok</b>, <b>Mistral</b> and <b>DeepSeek</b>. If you prefer to run things locally, <b>Ollama</b> and <b>llama.cpp</b> are supported too.</div><div><br></div><div>Prefer to hand the work to a professional translator? <b>XLIFF export and import</b> let you send the work out and bring it back in using their own tools.</div><div><br></div><div>To keep translations <b>reliable</b>, TMS FNC Localization <b>includes automatic validation</b> for format specifiers, placeholders, accelerator keys, line breaks and whitespace, whether a translation was typed by hand, imported, or generated by AI. It also supports <b>plural-aware translations</b>, so the correct wording is used depending on the quantity being displayed. That matters especially for languages where pluralization rules go well beyond a simple singular/plural split.</div><div><br></div><div>Beyond editing and verifying translations, TTMSFNCLocalizationEditor also lets you <b>manage the list of supported languages</b> and <b>set exclusion rules</b> for what shouldn&#39;t be collected. Using the editor also means collection happens automatically, without extra code.</div><div><br></div><div>The editor <b>keeps the translation process separate</b> from the application itself. Adding a language or updating an existing translation becomes a matter of maintaining localization data, not changing forms or source code.</div><br></div><div><img src="https://www.tmssoftware.com/site/img/blog/tmsfnclocalizationblog2.png" style="width: 100%;" alt="TMS Software Delphi  Components tmsfnclocalization"><br></div><div><br></div><div><b>Apply</b></div><div><div><br></div><div><div>Finally, <b>TTMSFNCLocalizationLocalizer </b>applies the selected language to the application. Point it to the folder holding your localization files, call <b>TrySetLocale</b>, and it switches to the target language right away. That gives you control over exactly when and how a language gets set. If you&#39;d rather let users choose, TMS FNC Localization also ships with a ready-made language picker, <b>TTMSFNCLocalizationCombobox</b>, that you can drop straight into your forms.</div><div><br></div><div>Together, these components turn localization into a workflow you can repeat, rather than a one-off job. Adding another language later just means going through the same steps again.</div></div><div><br></div><div><img src="https://www.tmssoftware.com/site/img/blog/tmsfnclocalizationblog3.png" style="width: 100%;" alt="TMS Software Delphi  Components tmsfnclocalization"><br></div><div><br></div><div><br></div><h4>New or Existing, Localization Fits<br></h4><p>The easiest time to think about localization is before you write the first line of an application. From the start, you can keep your strings in one place with <b>TTMSFNCLocalizationStringCatalog</b>, so everything stays organized, central and easy to collect.</p><p>That said, one of the goals behind TMS FNC Localization is to make localization approachable not only for new projects, but also for applications that already exist. Localization no longer has to be an all-or-nothing decision made on day one. You can introduce it gradually and <b>grow it as the application grows</b>.<br><br></p><div><h4>Try the open beta</h4><div><div>The open beta is now available as a trial download from the <a href="https://www.tmssoftware.com/site/tmsfnclocalization.asp" target="_blank">TMS FNC Localization product page</a>. If you have a TMS ALL-ACCESS&nbsp;license, you can get it through TMS Smart Setup instead.</div><div><br></div></div><div>To help you get started, we have included demos with the installation and prepared documentation covering both the component API and the overall workflow.&nbsp;<a href="https://doc.tmssoftware.com/fnc/localization/" target="_blank">Click here</a> to open it on our new TMS Documentation site, the <a href="https://tmssoftware.com/site/blog.asp?post=2501" target="_blank">new home for all TMS product documentation</a>.</div><div><br></div><h4>Help Us Shape TMS FNC Localization</h4><div><div><div>Localization needs vary a lot between applications, so feedback from developers using this on real projects matters to us.</div><div><br></div><div>Try it on a new application, or on that older project someone just decided needs to reach three new markets. Let us know what works, what could work better, and what&#39;s missing from your localization workflow.</div><br></div><div>Feedback and findings can be shared with us through the dedicated <a href="https://support.tmssoftware.com/c/fnc/tms-fnc-localization/131" target="_blank">TMS Support Center category</a>. We are looking forward to seeing what you build with it and in how many languages!</div></div></div></div></div></div></div>
		
		
		
		
		
		
		
		
		
		
		
		<br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿Keeping up with the latest changes in AI with TMS AI Studio 2.0]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2519</link>
		<author>Bradley Velghe</author>
		<pubDate>Thu, 17 Sep 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		
		<p>AI is moving at a pace that&#39;s hard to keep up with &#151; new models every few months, new providers, new ways for an assistant to actually <i>do</i> something instead of just answering questions. Delphi developers shouldn&#39;t have to sit on the sidelines of that while everyone else builds in Python or JavaScript. That&#39;s the whole premise of <b>TMS AI Studio</b>: bring modern AI &#151; cloud models, local models, and the tool-calling standard that ties them to real applications &#151; into native Delphi, with one consistent way of working across all of it.</p>

<p><b>Version 2.0</b> pushes on every one of those fronts at once. It keeps pace with the fastest-moving part of the AI world &#151; <b>MCP</b>, the Model Context Protocol that lets assistants like Claude and ChatGPT call into your own tools &#151; while adding a major new AI provider, OpenRouter.ai, to the growing list you can already reach from a single component. And crucially, none of it costs you a rewrite: everything you built on an earlier version keeps working exactly as it did.</p>

<div style="display:flex;flex-wrap:wrap;gap:14px;margin:22px 0;">
  <div style="flex:1;min-width:150px;background:linear-gradient(145deg,#131A24,#0B2E33);border:1px solid #17414A;border-radius:12px;padding:16px 14px;text-align:center;box-shadow:0 4px 18px rgba(14,124,134,0.18);">
    <div style="font-family:Consolas,monospace;font-size:26px;font-weight:700;background:linear-gradient(90deg,#22D3EE,#0E7C86);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:#0E7C86;">5</div>
    <div style="font-family:Consolas,monospace;font-size:10.5px;color:#9FB0C0;margin-top:4px;">MCP protocol revisions supported &#151; every one published</div>
  </div>
  <div style="flex:1;min-width:150px;background:linear-gradient(145deg,#131A24,#1B1533);border:1px solid #2E2361;border-radius:12px;padding:16px 14px;text-align:center;box-shadow:0 4px 18px rgba(124,92,252,0.18);">
    <div style="font-family:Consolas,monospace;font-size:26px;font-weight:700;background:linear-gradient(90deg,#A78BFA,#7C5CFC);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:#7C5CFC;">&lt;2yrs</div>
    <div style="font-family:Consolas,monospace;font-size:10.5px;color:#9FB0C0;margin-top:4px;">how old MCP itself is &#151; and we&#39;re already fully current</div>
  </div>
  <div style="flex:1;min-width:150px;background:linear-gradient(145deg,#131A24,#2A1B08);border:1px solid #5B3B0E;border-radius:12px;padding:16px 14px;text-align:center;box-shadow:0 4px 18px rgba(245,158,11,0.18);">
    <div style="font-family:Consolas,monospace;font-size:26px;font-weight:700;background:linear-gradient(90deg,#FCD34D,#F59E0B);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:#F59E0B;">10</div>
    <div style="font-family:Consolas,monospace;font-size:10.5px;color:#9FB0C0;margin-top:4px;">AI providers behind one component, cloud and local</div>
  </div>
  <div style="flex:1;min-width:150px;background:linear-gradient(145deg,#131A24,#0B2A20);border:1px solid #185C42;border-radius:12px;padding:16px 14px;text-align:center;box-shadow:0 4px 18px rgba(16,185,129,0.18);">
    <div style="font-family:Consolas,monospace;font-size:26px;font-weight:700;background:linear-gradient(90deg,#34D399,#10B981);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:#10B981;">0</div>
    <div style="font-family:Consolas,monospace;font-size:10.5px;color:#9FB0C0;margin-top:4px;">lines of existing code you need to change to upgrade</div>
  </div>
</div><p><br></p>

<h3>Always on the newest revision of MCP</h3>

<p>The real headline here is the newest revision on the block: <b>2026-07-28</b>, published only weeks ago, is already fully supported &#151; not on a roadmap, not "coming soon," but there today, on both the server and client side. And it doesn&#39;t stand alone: everything the spec has added since the very first release in late 2024 &#151; five revisions in total &#151; is supported right alongside it, so nothing an older assistant relies on stops working the moment a newer one shows up.</p>

<p>That&#39;s what staying current with MCP actually means in practice: not scrambling to catch up every time the spec moves, but already being there when it does.</p>

<p><svg viewBox="0 0 860 270" role="img" aria-label="Five MCP protocol revisions released within under two years, from 2024-11-05 to 2026-07-28, each with its headline feature, all converging into TMS AI Studio v2.0 which negotiates the highest one both sides support." style="max-width:100%;height:auto;">
  <defs>
    <linearGradient id="v1" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#1E1B4B"></stop><stop offset="100%" stop-color="#312E81"></stop></linearGradient>
    <linearGradient id="v2" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#1E3A5F"></stop><stop offset="100%" stop-color="#1D4ED8"></stop></linearGradient>
    <linearGradient id="v3" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#0B4A52"></stop><stop offset="100%" stop-color="#0E7C86"></stop></linearGradient>
    <linearGradient id="v4" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#065F46"></stop><stop offset="100%" stop-color="#10B981"></stop></linearGradient>
    <linearGradient id="v5" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#B45309"></stop><stop offset="100%" stop-color="#F59E0B"></stop></linearGradient>
    <linearGradient id="flowLine" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#4F46E5"></stop><stop offset="50%" stop-color="#0E7C86"></stop><stop offset="100%" stop-color="#F59E0B"></stop></linearGradient>
    <linearGradient id="summaryGrad" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#111827"></stop><stop offset="55%" stop-color="#0B2E33"></stop><stop offset="100%" stop-color="#1B1533"></stop></linearGradient>
    <marker id="mcpArrow" markerWidth="9" markerHeight="9" refX="4.5" refY="7" orient="auto"><path d="M0,0 L9,0 L4.5,7 z" fill="#F59E0B"></path></marker>
  </defs>

  <rect x="10" y="20" width="152" height="88" rx="10" fill="url(#v1)"></rect>
  <text x="86" y="42" text-anchor="middle" font-family="Consolas,monospace" font-size="11" font-weight="700" fill="#E0E7FF">2024-11-05</text>
  <text x="86" y="62" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#C7D2FE">MCP is born &#151;</text>
  <text x="86" y="78" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#C7D2FE">tools, resources,</text>
  <text x="86" y="92" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#C7D2FE">prompts, sampling</text>

  <rect x="177" y="20" width="152" height="88" rx="10" fill="url(#v2)"></rect>
  <text x="253" y="42" text-anchor="middle" font-family="Consolas,monospace" font-size="11" font-weight="700" fill="#DBEAFE">2025-03-26</text>
  <text x="253" y="62" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#BFDBFE">assistants can ask</text>
  <text x="253" y="78" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#BFDBFE">for suggestions &amp;</text>
  <text x="253" y="92" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#BFDBFE">track progress</text>

  <rect x="344" y="20" width="152" height="88" rx="10" fill="url(#v3)"></rect>
  <text x="420" y="42" text-anchor="middle" font-family="Consolas,monospace" font-size="11" font-weight="700" fill="#CFFAFE">2025-06-18</text>
  <text x="420" y="62" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#A5F3FC">servers can push</text>
  <text x="420" y="78" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#A5F3FC">updates &amp; ask the</text>
  <text x="420" y="92" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#A5F3FC">user for input</text>

  <rect x="511" y="20" width="152" height="88" rx="10" fill="url(#v4)"></rect>
  <text x="587" y="42" text-anchor="middle" font-family="Consolas,monospace" font-size="11" font-weight="700" fill="#D1FAE5">2025-11-25</text>
  <text x="587" y="62" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#A7F3D0">long jobs run in</text>
  <text x="587" y="78" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#A7F3D0">the background instead</text>
  <text x="587" y="92" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#A7F3D0">of blocking the chat</text>

  <rect x="678" y="20" width="172" height="88" rx="10" fill="url(#v5)"></rect>
  <text x="764" y="42" text-anchor="middle" font-family="Consolas,monospace" font-size="11" font-weight="700" fill="#fff">2026-07-28</text>
  <text x="764" y="62" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#FEF3C7">newest revision &#151;</text>
  <text x="764" y="78" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#FEF3C7">connections no longer</text>
  <text x="764" y="92" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#FEF3C7">need to stay open</text>

  <path d="M86,108 L86,130 L764,130 L764,108" stroke="url(#flowLine)" stroke-width="2" fill="none"></path>
  <path d="M253,108 L253,130" stroke="url(#flowLine)" stroke-width="2" fill="none"></path>
  <path d="M420,108 L420,130" stroke="url(#flowLine)" stroke-width="2" fill="none"></path>
  <path d="M587,108 L587,130" stroke="url(#flowLine)" stroke-width="2" fill="none"></path>
  <path d="M425,130 L425,160" stroke="url(#flowLine)" stroke-width="2.5" fill="none" marker-end="url(#mcpArrow)"></path>

  <rect x="130" y="166" width="600" height="82" rx="14" fill="url(#summaryGrad)" stroke="#0E7C86" stroke-width="1"></rect>
  <text x="430" y="196" text-anchor="middle" font-family="Consolas,monospace" font-size="12.5" font-weight="700" fill="#F1F5F9">TMS AI Studio v2.0</text>
  <text x="430" y="215" text-anchor="middle" font-family="Consolas,monospace" font-size="10" fill="#9FB0C0">understands all five revisions, on both server and client</text>
  <text x="430" y="231" text-anchor="middle" font-family="Consolas,monospace" font-size="10" fill="#9FB0C0">and always settles on the newest one both sides know</text>
</svg></p>
<p><i>Under two years old, five revisions in, and TMS AI Studio hasn&#39;t missed one.</i></p><p><br></p>

<div style="background:linear-gradient(120deg,#0B2E33,#1B1533);border-radius:8px;padding:18px 22px;margin:22px 0;border:1px solid #0E7C86;box-shadow:0 4px 20px rgba(79,70,229,0.15);position:relative;">
  <div style="position:absolute;left:0;top:0;bottom:0;width:4px;background:linear-gradient(180deg,#0E7C86,#7C5CFC);border-radius:8px 0 0 8px;"></div>
  <p style="margin:0;font-family:Consolas,monospace;font-size:14px;font-style:italic;color:#E5E7EB;">"Whatever assistant connects to your server tomorrow, it&#39;s already speaking a dialect TMS AI Studio understood yesterday."</p>
</div><p><br></p>

<h3>Upgrade today. Nothing breaks.</h3>

<p>None of the above is something you opt into feature by feature. Recompile an existing MCP server or client against v2.0, and it&#39;s simply fluent in more of the protocol than it was before &#151; because the server and whatever connects to it always agree, automatically, on the newest dialect they both happen to know. An older assistant still gets a correct conversation; a newer one gets everything the latest revision has to offer. Your code doesn&#39;t need to know the difference:</p>

<pre class="delphi" name="code">// exactly the code you already have
Server := TTMSMCPServer.Create(Self);
Server.Transport := Transport;
Server.RegisterTool(&#39;get_weather&#39;, GetWeatherHandler);

// on v2.0, this same server now negotiates
// protocol revision 2026-07-28 with any client that supports it &#151;
// nothing above this comment changed to make that happen</pre>

<p>The same is true if you&#39;re building the other end of that connection. A <code>TTMSMCPClient</code> you already wrote does exactly the same negotiation, automatically, the moment it connects:</p>

<pre class="delphi" name="code">// exactly the code you already have, on the client side
var
  Server: TTMSMCPClientServerItem;
begin
  Client := TTMSMCPClient.Create(Self);
  Server := Client.Servers.Add;
  Server.TransportType := ttHTTP;
  Server.URL := &#39;https://your-mcp-server.com/mcp&#39;;
  Server.Start;
end;

// on v2.0, this same client also negotiates the newest
// protocol revision the server understands &#151; again, nothing
// above this comment needed to change</pre><p><br></p>

<h3>Forward on both sides of the conversation</h3>

<p>It&#39;s easy for a release like this to quietly favor one side &#151; the server catches up, the client lags behind. Not here. Both directions move forward together, and a couple of smaller conveniences arrive with them: connected assistants now find out immediately when you add or remove a tool, resource, or prompt &#151; no extra call required &#151;</p>

<pre class="delphi" name="code">Server.Tools.RegisterTool(&#39;add_note&#39;, AddNoteHandler);
// that&#39;s it &#151; every connected assistant is told about
// the new tool automatically, the moment it&#39;s registered</pre>

<p>&#151; and sign-in now works the same way in both directions, as the next section covers.</p><p><br></p>

<h3>OAuth in the client: signing in without writing a sign-in flow</h3>

<p>A server-side login screen is only half of OAuth. The other half is a client that knows how to react when a server says "not so fast" &#151; and until now, that side of the conversation was something you had to build yourself. In v2.0, <code>TTMSMCPClient</code> does it for you.</p>

<p>When a protected MCP server answers with "unauthorized," the client&#39;s transport notices on its own, works out which login service to trust, opens the system browser for the user to sign in and approve access, and &#151; once that&#39;s done &#151; retries the original request with the token it just received. No callback to wire up, no token storage to write, no retry logic of your own:</p>

<pre class="delphi" name="code">var
  Server: TTMSMCPClientServerItem;
begin
  Client := TTMSMCPClient.Create(Self);
  Server := Client.Servers.Add;
  Server.TransportType := ttHTTP;
  Server.URL := &#39;https://your-mcp-server.com/mcp&#39;;
  Server.Start;
end;

// if the server responds "unauthorized", the client opens a
// browser, waits for the user to approve, and quietly retries &#151;
// nothing above this line needed to know a login was coming</pre>

<p><svg viewBox="0 0 860 150" role="img" aria-label="A protected MCP server refuses a request, the client automatically opens a browser for sign-in, and then retries the request successfully, with no code written for any of it." style="max-width:100%;height:auto;">
  <defs>
    <linearGradient id="oaStep1" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#312E81"></stop><stop offset="100%" stop-color="#4F46E5"></stop></linearGradient>
    <linearGradient id="oaStep2" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#0B4A52"></stop><stop offset="100%" stop-color="#0E7C86"></stop></linearGradient>
    <linearGradient id="oaStep3" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#065F46"></stop><stop offset="100%" stop-color="#10B981"></stop></linearGradient>
    <linearGradient id="oaFlow" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#4F46E5"></stop><stop offset="50%" stop-color="#0E7C86"></stop><stop offset="100%" stop-color="#10B981"></stop></linearGradient>
    <marker id="oaArrow" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#0E7C86"></path></marker>
  </defs>

  <rect x="10" y="35" width="250" height="80" rx="12" fill="url(#oaStep1)"></rect>
  <text x="135" y="68" text-anchor="middle" font-family="Consolas,monospace" font-size="12" font-weight="700" fill="#E0E7FF">401 Unauthorized</text>
  <text x="135" y="86" text-anchor="middle" font-family="Consolas,monospace" font-size="9.5" fill="#C7D2FE">server refuses the request &#151;</text>
  <text x="135" y="100" text-anchor="middle" font-family="Consolas,monospace" font-size="9.5" fill="#C7D2FE">client catches this automatically</text>

  <path d="M260,75 H298" stroke="url(#oaFlow)" stroke-width="2.5" fill="none" marker-end="url(#oaArrow)"></path>

  <rect x="300" y="35" width="260" height="80" rx="12" fill="url(#oaStep2)"></rect>
  <text x="430" y="60" text-anchor="middle" font-family="Consolas,monospace" font-size="12" font-weight="700" fill="#CFFAFE">Browser opens</text>
  <text x="430" y="78" text-anchor="middle" font-family="Consolas,monospace" font-size="9.5" fill="#A5F3FC">user signs in, approves access &#151;</text>
  <text x="430" y="94" text-anchor="middle" font-family="Consolas,monospace" font-size="9.5" fill="#A5F3FC">the same flow every OAuth login uses</text>

  <path d="M560,75 H598" stroke="url(#oaFlow)" stroke-width="2.5" fill="none" marker-end="url(#oaArrow)"></path>

  <rect x="600" y="35" width="250" height="80" rx="12" fill="url(#oaStep3)"></rect>
  <text x="725" y="60" text-anchor="middle" font-family="Consolas,monospace" font-size="12" font-weight="700" fill="#D1FAE5">Signed in</text>
  <text x="725" y="78" text-anchor="middle" font-family="Consolas,monospace" font-size="9.5" fill="#A7F3D0">the original request retries</text>
  <text x="725" y="94" text-anchor="middle" font-family="Consolas,monospace" font-size="9.5" fill="#A7F3D0">with the token &#151; automatically</text>
</svg></p>
<p><i>Three steps, zero lines of sign-in code &#151; the client handles all of it on its own.</i></p><p><br></p>

<p>Want a hand on the wheel anyway? A handful of events let you step in exactly where it helps &#151; open the sign-in page in your own embedded browser control instead of the system one, choose which login service to use when a server offers more than one, or simply get notified the moment a token comes back. Leave them alone, and the client still signs in correctly without you touching any of it.</p><p><br></p>

<h3>One gateway, hundreds of AI models: OpenRouter.ai support</h3>

<p>With the latest extension to TMS AI Studio, Delphi developers can now also use OpenRouter.ai as a cloud LLM service. Switching to OpenRouter is as simple as changing a single property:</p>

<pre class="delphi" name="code">TMSMCPCloudAI1.Service := aiOpenRouter;</pre>

<p>From that point on, TMS AI Studio takes care of the service integration, allowing your Delphi application to access the wide range of AI models available through OpenRouter&#39;s unified API.</p>

<p>The model to use can be selected through:</p>

<pre class="delphi" name="code">TMSMCPCloudAI1.Settings.OpenRouterModel := &#39;z-ai/glm-5.3-flash&#39;;</pre>

<p>The default model is <code>z-ai/glm-5.3-flash</code>, which offers an attractive combination of high performance and low usage cost. In fact, attractive pricing is one of the interesting aspects of using OpenRouter.ai. It provides access to a very broad selection of models, with many capable models available at remarkably low token prices. This makes OpenRouter particularly interesting for developers who want to experiment with different LLMs, as well as for applications where AI is used frequently and inference cost therefore becomes an important consideration.</p>

<p>Of course, another major strength of OpenRouter.ai is that you are not tied to a single model provider. Its catalog provides access to a large and continuously evolving selection of models, including popular model families from OpenAI, Anthropic Claude, Google Gemini, Meta Llama, DeepSeek, Qwen, Mistral, xAI Grok, Z.AI GLM, and many others. This makes it particularly convenient to experiment with different models or select the best model for a particular task without having to implement a different API integration for every AI provider.</p>

<p>Another interesting OpenRouter feature, especially for professional and European users, is its support for data residency and provider routing controls. With the appropriate OpenRouter business offering, routing can be restricted to infrastructure in specific regions, such as EU-only or US-only processing. This can be important when considering data privacy, regulatory requirements and data sovereignty policies.</p>

<p>And this is precisely where TMS AI Studio becomes especially powerful. OpenRouter.ai is yet another service that plugs into the same Delphi-friendly architecture:</p>

<p><svg viewBox="0 0 860 300" role="img" aria-label="TTMSMCPCloudAI as a single hub component connecting to ten cloud and local AI providers, including the newly added OpenRouter." style="max-width:100%;height:auto;">
  <defs>
    <radialGradient id="hubGlow" cx="50%" cy="50%" r="65%"><stop offset="0%" stop-color="#22D3EE"></stop><stop offset="55%" stop-color="#0E7C86"></stop><stop offset="100%" stop-color="#0B4A52"></stop></radialGradient>
    <linearGradient id="providerGrad" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#131A24"></stop><stop offset="100%" stop-color="#1B2733"></stop></linearGradient>
    <linearGradient id="localGrad" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#111827"></stop><stop offset="100%" stop-color="#1E2A1E"></stop></linearGradient>
    <linearGradient id="orGrad" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#0E7C86"></stop><stop offset="100%" stop-color="#7C5CFC"></stop></linearGradient>
    <linearGradient id="spokeGrad" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#0E7C86"></stop><stop offset="100%" stop-color="#475569"></stop></linearGradient>
  </defs>

  <path d="M330,150 L69,58" stroke="url(#spokeGrad)" stroke-width="1.25" fill="none"></path>
  <path d="M350,140 L199,58" stroke="url(#spokeGrad)" stroke-width="1.25" fill="none"></path>
  <path d="M380,132 L329,58" stroke="url(#spokeGrad)" stroke-width="1.25" fill="none"></path>
  <path d="M480,132 L531,58" stroke="url(#spokeGrad)" stroke-width="1.25" fill="none"></path>
  <path d="M510,140 L661,58" stroke="url(#spokeGrad)" stroke-width="1.25" fill="none"></path>
  <path d="M530,150 L791,58" stroke="url(#spokeGrad)" stroke-width="1.25" fill="none"></path>

  <path d="M330,156 L69,242" stroke="url(#spokeGrad)" stroke-width="1.25" fill="none"></path>
  <path d="M350,168 L199,242" stroke="url(#spokeGrad)" stroke-width="1.25" fill="none"></path>
  <path d="M380,176 L340,242" stroke="url(#spokeGrad)" stroke-width="1.25" fill="none"></path>
  <path d="M530,156 L726,242" stroke="url(#orGrad)" stroke-width="2.25" fill="none"></path>

  <g font-family="Consolas,monospace" font-size="10" fill="#D7DEE6">
    <rect x="10" y="18" width="118" height="40" rx="8" fill="url(#providerGrad)" stroke="#1F2937"></rect>
    <text x="69" y="42" text-anchor="middle">aiOpenAI</text>
    <rect x="140" y="18" width="118" height="40" rx="8" fill="url(#providerGrad)" stroke="#1F2937"></rect>
    <text x="199" y="42" text-anchor="middle">aiClaude</text>
    <rect x="270" y="18" width="118" height="40" rx="8" fill="url(#providerGrad)" stroke="#1F2937"></rect>
    <text x="329" y="42" text-anchor="middle">aiGemini</text>
    <rect x="472" y="18" width="118" height="40" rx="8" fill="url(#providerGrad)" stroke="#1F2937"></rect>
    <text x="531" y="42" text-anchor="middle">aiGrok</text>
    <rect x="602" y="18" width="118" height="40" rx="8" fill="url(#providerGrad)" stroke="#1F2937"></rect>
    <text x="661" y="42" text-anchor="middle">aiPerplexity</text>
    <rect x="732" y="18" width="118" height="40" rx="8" fill="url(#providerGrad)" stroke="#1F2937"></rect>
    <text x="791" y="42" text-anchor="middle">aiMistral</text>

    <rect x="10" y="242" width="118" height="40" rx="8" fill="url(#localGrad)" stroke="#1F2937"></rect>
    <text x="69" y="266" text-anchor="middle">aiDeepSeek</text>
    <rect x="140" y="242" width="118" height="40" rx="8" fill="url(#localGrad)" stroke="#1F2937"></rect>
    <text x="199" y="266" text-anchor="middle">aiOllama</text>
    <rect x="270" y="242" width="140" height="40" rx="8" fill="url(#localGrad)" stroke="#1F2937"></rect>
    <text x="340" y="266" text-anchor="middle">aiLlamaCpp</text>
    <text x="340" y="278" text-anchor="middle" font-size="8" fill="#6EE7B7">local model</text>
    <rect x="602" y="242" width="248" height="40" rx="8" fill="url(#orGrad)"></rect>
    <text x="726" y="266" text-anchor="middle" font-weight="700" fill="#ffffff">aiOpenRouter</text>
    <text x="726" y="278" text-anchor="middle" font-size="8" fill="#EDE9FE">new in v2.0 &#151; hundreds of models</text>
  </g>

  <rect x="330" y="120" width="200" height="66" rx="12" fill="url(#hubGlow)"></rect>
  <text x="430" y="148" text-anchor="middle" font-family="Consolas,monospace" font-size="12" font-weight="700" fill="#062024">TTMSMCPCloudAI</text>
  <text x="430" y="166" text-anchor="middle" font-family="Consolas,monospace" font-size="9.5" fill="#04333A">one component, one API</text>
</svg></p>
<p><i>Ten AI providers, cloud and local, behind one consistent component. OpenRouter is simply the newest door into that same room.</i></p><p><br></p>

<p>With the meanwhile extensive range of directly supported cloud LLM services, local LLMs and hundreds of available models, developers can integrate AI into Delphi applications through one consistent component API. Changing from a local model to a cloud model&#151;or experimenting with an entirely different model family&#151;can often be reduced to changing just a service or model property, while the rest of the application code remains unchanged.</p>

<p>With OpenRouter.ai added to TMS AI Studio, Delphi developers gain even more choice in models, providers, performance and pricing, while continuing to work with the same familiar TMS AI Studio component architecture.</p>

<p>More models, more providers, attractive AI pricing and more deployment choices&#151;without adding more complexity to your Delphi application.</p><p><br></p>

<div style="display:flex;flex-wrap:wrap;gap:14px;margin:22px 0;">
  <div style="flex:1;min-width:150px;background:linear-gradient(145deg,#131A24,#0B2E33);border:1px solid #17414A;border-radius:12px;padding:16px 14px;text-align:center;box-shadow:0 4px 18px rgba(14,124,134,0.18);">
    <div style="font-family:Consolas,monospace;font-size:20px;font-weight:700;background:linear-gradient(90deg,#22D3EE,#0E7C86);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:#0E7C86;">1 property</div>
    <div style="font-family:Consolas,monospace;font-size:10.5px;color:#9FB0C0;margin-top:4px;">to switch AI service entirely</div>
  </div>
  <div style="flex:1;min-width:150px;background:linear-gradient(145deg,#131A24,#1B1533);border:1px solid #2E2361;border-radius:12px;padding:16px 14px;text-align:center;box-shadow:0 4px 18px rgba(124,92,252,0.18);">
    <div style="font-family:Consolas,monospace;font-size:20px;font-weight:700;background:linear-gradient(90deg,#A78BFA,#7C5CFC);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:#7C5CFC;">hundreds</div>
    <div style="font-family:Consolas,monospace;font-size:10.5px;color:#9FB0C0;margin-top:4px;">of models reachable through OpenRouter alone</div>
  </div>
  <div style="flex:1;min-width:150px;background:linear-gradient(145deg,#131A24,#0B2A20);border:1px solid #185C42;border-radius:12px;padding:16px 14px;text-align:center;box-shadow:0 4px 18px rgba(16,185,129,0.18);">
    <div style="font-family:Consolas,monospace;font-size:20px;font-weight:700;background:linear-gradient(90deg,#34D399,#10B981);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:#10B981;">cloud + local</div>
    <div style="font-family:Consolas,monospace;font-size:10.5px;color:#9FB0C0;margin-top:4px;">same API whether the model runs remotely or on your machine</div>
  </div>
</div><p><br></p>

<h3>Easier to learn, easier to build on</h3>

<p>Alongside the release itself, a few things changed in how you actually get to it. Full <a href="https://doc.tmssoftware.com/dev/aistudio">documentation</a> for everything in v2.0 is already up, covering the protocol negotiation, the client-side OAuth flow, and every CloudAI provider in detail. TMS AI Studio also <a href="https://www.tmssoftware.com/site/blog.asp?post=2495">joins the new install experience</a> introduced across TMS products, and as part of that, the demos move to <a href="https://github.com/tmssoftware/tms.dev.aistudio.demos">their own GitHub repository</a> &#151; so you can freely edit them, keep your changes under version control, and not have to worry about a future install or Smart Setup run quietly overwriting the copy you&#39;ve been working in.</p><p><br></p>

<h3>What v2.0 adds up to</h3>

<p>AI is changing fast, on every front &#151; new models, new providers, new standards for how assistants reach real applications. TMS AI Studio v2.0 is about making sure Delphi doesn&#39;t have to wait to benefit from any of it. Your existing MCP servers and clients keep working, unchanged, while gaining everything the newest protocol revision has to offer, on both ends of the connection. Signing in to a protected server is no longer something you build by hand on either side. And reaching a wider, cheaper, ever-growing spread of AI models is now a one-line change rather than a new integration.</p><p><br></p>

<hr>

<p><b><a href="https://www.tmssoftware.com/site/tmsaistudio.asp">TMS AI Studio</a></b> is the complete toolkit for bringing AI and the Model Context Protocol to your Delphi applications. Build MCP servers and clients, connect Claude and other assistants to your own tools and data, and add cloud AI to your VCL and FMX apps &#151; all in native Delphi. <a href="https://www.tmssoftware.com/site/tmsaistudio.asp">Discover TMS AI Studio</a>.</p>
		
		
		
		<br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿Data Report with Filtering and Conditional Formatting]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2515</link>
		<author>Gjalt Vanhouwaert</author>
		<pubDate>Tue, 8 Sep 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		
		
		<p> In our previous blog post, <a href="https://tmssoftware.com/site/blog.asp?post=2503" target="_blank"> Creating a Database Report Generator with TMS FNC DataGrid </a>, we created a flexible report generator capable of loading database data and exporting the current view to several different formats. </p>

<p> This time, we&#39;re taking that same project a step further. </p>

<p> Instead of focusing on generating the report, we want to give the user more control over <b>what they see and how they see it</b>. </p>

<p>For this, we&#39;ve added:</p>

<ul> <li>Filtering</li> <li>Advanced filtering</li> <li>Conditional formatting</li> <li>A built-in conditional formatting editor</li> <li>Saving and loading formatting and filter settings</li></ul><ul> </ul>

<br><div style="text-align: center;"><a href="https://github.com/tmssoftware/fnc_database_report_generator"><p><span style="background-color: #12237f; color: white; padding: 16px 32px; text-align: center; border-radius: 6px; font-size: 1.35em;">Free Sample</span></p></a></div><br>

<p> What makes these additions particularly interesting is not only what they add to the application, but <b>how little code is actually required to enable them</b>.</p><p><img src="https://www.tmssoftware.com/site/img/blog/FNCDBReportGenerator/FNCDBReporter.png" style="width: 829px;" alt="TMS Software Delphi  Components tmsfncuipack"><br></p>

<p> A lot of this functionality is already built directly into <code>TTMSFNCDataGrid</code>. </p>

<h3>Starting from our Report Generator</h3>

<p> We&#39;ll continue with the database report generator from the <a href="https://tmssoftware.com/site/blog.asp?post=2503" target="_blank">previous blog post</a>. </p>

<p> That project already takes care of connecting to the database, displaying the data in <code>TTMSFNCDataGrid</code>, and exporting the resulting report. We won&#39;t repeat that implementation here. </p>

<p> Instead, we&#39;ll focus entirely on making that data more interactive. </p>

<h3>Adding Filtering: One Line</h3>

<p> Giving users the ability to filter the data displayed in the grid requires: </p>

<pre><code class="language-delphi">Grid.Options.Filtering.Enabled := True;</code></pre>

<p> That&#39;s it. </p>

<p> With one property, the DataGrid gets its filtering interface. </p>

<p> In the sample, we&#39;ve exposed this through a checkbox: </p>

<pre><code class="language-delphi">Grid.Options.Filtering.Enabled := CheckFiltering.IsChecked;</code></pre>

<p> The user can now enable or disable filtering without us having to create a separate filtering interface, parse values or manually build a filtering system around the grid. </p>

<p> This is a good example of the low-code approach behind the DataGrid: <b>enable the capability and let the component provide the UI and behaviour.</b> </p>

<h3>Need More? Enable Advanced Filtering</h3>

<p> For more complex datasets, a simple filter value isn&#39;t always enough. </p>

<p> A user might want to specify whether a value should be equal to, greater than, smaller than or otherwise compared against another value. </p>

<p> For that, we can switch the existing filtering functionality into its advanced mode: </p>

<pre><code class="language-delphi">Grid.Options.Filtering.Advanced := True;</code></pre>

<p> Again, we&#39;re talking about <b>one line of Delphi code</b>. </p>

<p> Instead of building an advanced filter interface ourselves, the DataGrid provides the richer operator-based filtering UI. </p>

<p> In our application, the user can switch between both modes: </p>

<pre><code class="language-delphi">Grid.Options.Filtering.Advanced := CheckAdvancedFilter.IsChecked;</code></pre>

<p> This means we can progressively expose more functionality without making the basic interface more complicated than necessary. </p>

<p> Enable filtering when you need it. Enable advanced filtering when you need more control. </p>

<p> No separate filtering system needs to be developed. </p>
<p style="text-align: center; "><img src="https://www.tmssoftware.com/site/img/blog/FNCDBReportGenerator/FNCDBReporterFilter.png" style="width:70%;" alt="TMS Software Delphi  Components tmsfncuipack"><br></p>

<h3>Conditional Formatting: A Brand-New DataGrid Feature</h3>

<p> Filtering helps users find the information they&#39;re interested in. Conditional formatting helps them <b>see important information immediately</b>. </p>

<p> Conditional formatting is a brand-new feature in <code>TTMSFNCDataGrid</code>. We introduced the feature in more detail in: </p>

<p> <a href="https://tmssoftware.com/site/blog.asp?post=2514" target="_blank"> <b>our dedicated conditional formatting blog post</b> </a>. </p>

<p> If you want to learn more about the available rules and how conditional formatting works internally, that blog post is the best place to start. Here, we&#39;ll focus on how easily we can add it to our existing report generator. </p>

<p> Enabling conditional formatting requires: </p>

<pre><code class="language-delphi">Grid.ConditionalFormatting.Enabled := True;</code></pre>

<p> Once again, one line is enough to enable an entirely new layer of functionality in our application. </p>

<p> Conditional formatting can then be used to visually distinguish values using rules such as: </p>

<ul> <li>Data bars</li> <li>Colour scales</li> <li>Top-N values</li> <li>Value-based highlights</li> </ul>

<p> This can turn a regular table of database records into something much easier to scan and interpret.</p><p style="text-align: center; "><img src="https://www.tmssoftware.com/site/img/blog/FNCDBReportGenerator/FNCDBReporterFormat.png" style="width:70%;" alt="TMS Software Delphi  Components tmsfncuipack"><br></p><p> </p>

<h3>Let the User Create the Rules</h3>

<p> Enabling conditional formatting is useful, but we don&#39;t necessarily want to decide all the rules beforehand. </p>

<p> The person using the report may know much better which values deserve attention. </p>

<p> So why not give them access to the built-in conditional formatting editor? </p>

<pre><code class="language-delphi">Grid.Root.ShowConditionalFormattingEditor;</code></pre>

<p> With this single call, we can open the DataGrid&#39;s conditional formatting editor. </p>

<p> Users can add, remove and configure their own conditional formatting rules at runtime. </p>


<p> That means we don&#39;t need to create a custom rule editor, separate forms for the different rule types, or the logic required to maintain all those rules ourselves.</p>

<pre><code class="language-delphi">procedure TMainForm.ButtonEditRulesClick(Sender: TObject); <br>begin <br>  Grid.Root.ShowConditionalFormattingEditor; <br>end;</code></pre>

<p> This is where the low-code aspect becomes particularly valuable. </p>

<p> We&#39;re not simply enabling a visual effect with one line of code. We&#39;re exposing an <b>entire configuration interface</b> with it. </p>

<h3>Configure Once, Use Again</h3>

<p> After spending time configuring filters and conditional formatting, users probably don&#39;t want to recreate everything the next time they start the application. </p>

<p> So the sample also adds the ability to save the current configuration. </p>

<p> For conditional formatting, <code>TTMSFNCDataGrid</code> already provides serialization of its rules: </p>

<pre><code class="language-delphi">Grid.ConditionalFormatting.SaveToJSONStream(Stream);</code></pre>

<p> The resulting JSON can later be loaded again with: </p>

<pre><code class="language-delphi">Grid.ConditionalFormatting.LoadFromJSONStream(Stream);</code></pre>

<p> The sample stores the conditional formatting configuration in: </p>

<pre><code>FNCDBReporterFormatting.json</code></pre>

<p> For filtering, we can retrieve the current filter expression through: </p>

<pre><code class="language-delphi">Grid.Filter.FilterText</code></pre>

<p> The sample stores this expression separately in: </p>

<pre><code>FNCDBReporterFilter.ini</code></pre>

<p> When the application starts again, the saved expression can be assigned back to the FilterBuilder: </p>

<pre><code class="language-delphi">Grid.FilterBuilder.FilterText := FilterText;</code></pre>

<p> and applied: </p>

<pre><code class="language-delphi">Grid.RemoveFilter; Grid.ApplyFilter;</code></pre>

<p> This gives us an easy way to restore a previously configured report view. </p>

<h3>Save Your Report View</h3>

<p> Together, these features introduce an interesting concept to our original report generator. </p>

<p> We&#39;re no longer just saving or exporting <b>data</b>. </p>

<p> We&#39;re allowing users to configure a <b>view of that data</b>. </p>

<p> For example, imagine a sales report where somebody: </p>

<ol> <li>Enables advanced filtering.</li> <li>Filters the report to a particular region or product range.</li> <li>Adds a conditional formatting rule to highlight high-value orders.</li> <li>Adds another rule to draw attention to low-performing values.</li> <li>Saves the configuration.</li> </ol>

<p> The next time the report is opened, that configuration can be restored and applied to the latest data. </p>

<p> The data may have changed, but the way the user wants to analyse it hasn&#39;t. </p>

<h3>A Lot of Functionality, Very Little Code</h3>

<p> This extension of the sample demonstrates something that is easy to overlook when looking at a feature list. </p>

<p> Adding filtering sounds simple until you have to create the filtering UI. Advanced filtering means operators, different value types and additional UI. Conditional formatting means defining rules, rendering them and preferably providing an editor. Saving those settings means serialization and restoration. </p>

<p> With <code>TTMSFNCDataGrid</code>, the core functionality is already available. </p>

<p> The essential calls are remarkably small: </p>

<pre><code class="language-delphi">// Filtering <br>Grid.Options.Filtering.Enabled := True; <br>// Advanced filtering <br>Grid.Options.Filtering.Advanced := True; <br>// Conditional formatting <br>Grid.ConditionalFormatting.Enabled := True; <br>// Open the rule editor <br>Grid.Root.ShowConditionalFormattingEditor; <br>// Save formatting <br>Grid.ConditionalFormatting.SaveToJSONStream(Stream); <br>// Load formatting <br>Grid.ConditionalFormatting.LoadFromJSONStream(Stream);</code></pre>

<p> This is the real advantage of the approach. </p>

<p> <b>A few lines of Delphi code can expose functionality that would otherwise require complete interfaces and considerable application logic.</b> </p>

<p> You remain in control of when and where the functionality is available, while <code>TTMSFNCDataGrid</code> takes care of most of the implementation. </p>

<h3>From Static Report to Interactive Report</h3>

<p> Our original project demonstrated how <code>TTMSFNCDataGrid</code> could be used as the foundation of a database report generator. </p>

<p> With these additions, that report becomes much more interactive. </p>

<p> Users can decide which information matters, narrow down larger datasets, visually identify interesting values and preserve their preferred configuration for later use. </p>

<p> And perhaps most importantly for us as Delphi developers, we didn&#39;t have to build all those systems from scratch. </p>

<p> Sometimes adding a powerful feature really can be just <b>one line of code</b>. </p>
		
		
		
		
		<br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿Filtering in Delphi: Building a Highlighting Property-Based Search]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2510</link>
		<author>Gjalt Vanhouwaert</author>
		<pubDate>Mon, 7 Sep 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		<p>In the previous blog post, we introduced the new <b>TTMSFNCFilterRulesManager</b> and looked at how filtering can go beyond datasets and grid columns.</p>

<p>Instead of only filtering data, the Filter Rules Manager can evaluate properties of Delphi objects and use the result to change other properties or execute application logic.</p>

<p>We also showed the following example:</p>

<p style="text-align:center;"><img src="https://www.tmssoftware.com/site/img/blog/filterseries/filterrulesplannerexample.png" style="width: 828px;" alt="TMS Software Delphi  Components "><br></p>

<p>In this blog post, we&#39;re going to take a closer look at that example and see how it was built.</p>

<p>The application contains a <b>TTMSFNCPlanner</b> with several appointments and a search box at the top.</p>

<p>As the user types, appointments with a matching title keep their original appearance, while appointments that don&#39;t match are dimmed.</p>

<p>There is no dataset filter involved.</p>

<p>Instead, we&#39;re going to filter directly on the <code>Title</code> property of each <b>TTMSFNCPlannerItem</b>.</p><p><br></p>

<h3>Setting Up the Planner</h3>

<p>For this example, the Planner contains a number of appointments spread across the working week.</p>

<p>Adding an appointment is nothing special:</p>

<pre class="delphi" name="code">Item := TMSFNCPlanner1.Items.Add;
Item.Title := ATitle;
Item.Text := AText;
Item.StartTime := AStartTime;
Item.EndTime := AEndTime;
Item.Color := AFill;</pre>

<p>Some examples used in the demo are:</p>

<ul>
<li>Team Standup</li>
<li>Sprint Planning</li>
<li>Architecture Review</li>
<li>Design Review</li>
<li>Product Demo</li>
<li>Training Session</li>
<li>Sprint Retrospective</li>
</ul>

<p>What matters for our rule is that every appointment already has a <code>Title</code> property.</p>

<p>That&#39;s the property we&#39;re going to search.</p><p><br></p>

<h3>Creating the Filter Rules Manager</h3>

<p>We start by creating a <b>TTMSFNCFilterRulesManager</b>:</p>

<pre class="delphi" name="code">FRM := TTMSFNCFilterRulesManager.Create(Self);</pre>

<p>Next, we need to connect the text entered by the user to our rule.</p>

<p>We could recreate the filter expression every time the user types something, but there is a better option: <b>placeholders</b>.</p>

<h3>Adding a Dynamic Search Value</h3>

<p>A placeholder lets a rule retrieve a value from another object when the rule is evaluated.</p>

<p>Our search text comes from a <code>TEdit</code> called <code>SearchEdit</code>.</p>

<p>We register its <code>Text</code> property as a placeholder:</p>

<pre class="delphi" name="code">FRM.AddPlaceholder(
  &#39;Search&#39;,
  SearchEdit,
  &#39;Text&#39;
);</pre>

<p>From this point on, we can use:</p>

<pre class="delphi" name="code">{Search}</pre>

<p>inside our rule.</p>

<p>Whenever the rule is applied, <code>{Search}</code> is replaced with the current value of <code>SearchEdit.Text</code>.</p>

<p>This is an important detail: the value isn&#39;t fixed when the rule is created.</p>

<p>The same rule can therefore remain in place while the user continues typing.</p><p><br></p>

<h3>Creating the Rule</h3>

<p>Now we can create the actual rule:</p>

<pre class="delphi" name="code">FRule1 := FRM.AddRule(
  &#39;SpotlightRule&#39;,
  TMSFNCPlanner1,
  &#39;Items&#39;,
  &#39;[Title] LIKE &#39;&#39;%{Search}%&#39;&#39;&#39;
);</pre>

<p>This small piece of code contains most of the logic for our search.</p>

<p>Let&#39;s break it down.</p><p><br></p>

<h3>1. The Rule Name</h3>

<pre class="delphi" name="code">&#39;SpotlightRule&#39;</pre>

<p>Every rule has a name so it can be identified later.</p><p><br></p>

<h3>2. The Source</h3>

<pre class="delphi" name="code">TMSFNCPlanner1</pre>

<p>This is the object where the Filter Rules Manager starts.</p>

<p>But we don&#39;t want to evaluate the Planner itself. We want to evaluate its appointments.</p>

<p>That&#39;s where the next parameter comes in.</p><p><br></p>

<h3>3. The Items Path</h3>

<pre class="delphi" name="code">&#39;Items&#39;</pre>

<p>The Filter Rules Manager uses this path to retrieve the collection of objects that should be evaluated.</p>

<p>In this example, that means every <b>TTMSFNCPlannerItem</b> inside:</p>

<pre class="delphi" name="code">TMSFNCPlanner1.Items</pre>

<p>The rule is therefore applied to each appointment individually.</p><p><br></p>

<h3>4. The Filter Expression</h3>

<p>Finally, we have the actual condition:</p>

<pre class="delphi" name="code">&#39;[Title] LIKE &#39;&#39;%{Search}%&#39;&#39;&#39;</pre>

<p>This is where things become interesting.</p>

<p><code>Title</code> is not a field from a dataset.</p>

<p>It is the <b>Title property of TTMSFNCPlannerItem</b>.</p>

<p>For every planner item, the Rules Manager retrieves that property and evaluates it using the filter expression.</p>

<p>If the user enters:</p>

<pre class="delphi" name="code">Team</pre>

<p>the effective condition becomes:</p>

<pre class="delphi" name="code">[Title] LIKE &#39;%Team%&#39;</pre>

<p>Appointments such as <code>Team Standup</code> match, while appointments such as <code>Product Demo</code> don&#39;t.</p>

<p>This is the key difference with the filtering examples from earlier in this series.</p>

<p>We&#39;re using the same structured filtering concepts, but now we&#39;re evaluating properties directly on Delphi objects.</p><p><br></p>

<h3>What Should Happen When an Item Matches?</h3>

<p>We now know whether each appointment matches the search.</p>

<p>The next question is what we want to do with that information.</p>

<p>We could hide appointments that don&#39;t match.</p>

<p>But for this example, we want to create more of a <i>spotlight</i> effect.</p>

<p>Matching appointments remain unchanged, while the other appointments are visually dimmed.</p>

<p>For that, we use <code>CallBackAction</code>:</p>

<pre class="delphi" name="code">FRule1.CallBackAction :=
  procedure(
    const ARule: TTMSFNCFilterRule;
    AItem: TObject;
    AIndex: Integer;
    const AMatched: Boolean)
  var
    Item: TTMSFNCPlannerItem;
    Idx: Integer;
  begin
    Item := AItem as TTMSFNCPlannerItem;
    Idx := Item.Index;

    if AMatched then
    begin
      Item.Color := FColors[Idx].Fill;
      Item.StrokeColor := FColors[Idx].Stroke;
      Item.FontColor := FColors[Idx].Font;
      Item.TitleFontColor := FColors[Idx].Font;
      Item.MarkColor := FColors[Idx].Mark;
      Inc(FMatchCount);
    end
    else
    begin
      Item.Color := DimColor(FColors[Idx].Fill);
      Item.StrokeColor := DimColor(FColors[Idx].Stroke);
      Item.FontColor := DimColor(FColors[Idx].Font);
      Item.TitleFontColor := DimColor(FColors[Idx].Font);
      Item.MarkColor := DimColor(FColors[Idx].Mark);
    end;
  end;</pre>

<p>The callback is executed for every item processed by the rule.</p>

<p>The <code>AMatched</code> parameter immediately tells us whether the current planner item matched our <code>Title</code> condition.</p>

<p>When it matches, we restore its original colors.</p>

<p>When it doesn&#39;t, we apply a dimmed version of those colors.</p><p><br></p>

<h3>One Match, Multiple Property Changes</h3>

<p>This example also demonstrates another useful aspect of the Filter Rules Manager.</p>

<p>One condition can affect multiple properties.</p>

<p>For a non-matching appointment, we&#39;re changing:</p>

<ul>
<li><code>Color</code></li>
<li><code>StrokeColor</code></li>
<li><code>FontColor</code></li>
<li><code>TitleFontColor</code></li>
<li><code>MarkColor</code></li>
</ul>

<p>All of these changes are driven by the result of a single condition:</p>

<pre class="delphi" name="code">[Title] LIKE &#39;%{Search}%&#39;</pre>

<p>For this example we use a callback because we want precise control over several appearance properties and we also keep track of the number of matching items.</p>

<p>For simpler cases, the Filter Rules Manager also provides actions that can directly update properties without requiring a callback.</p><p><br></p>

<h3>Making the Search Live</h3>

<p>We have created the rule only once.</p>

<p>To turn it into a live search, all we need to do is reapply it when the text changes.</p>

<pre class="delphi" name="code">procedure TFormFilterRules.SearchEditChangeTracking(
  Sender: TObject);
begin
  UpdateFilter;
end;</pre>

<p>The <code>UpdateFilter</code> method itself remains very small:</p>

<pre class="delphi" name="code">procedure TFormFilterRules.UpdateFilter;
begin
  FMatchCount := 0;

  FRM.ApplyRule(FRule1);

  CountLbl.Text := Format(
    &#39;%d / %d&#39;,
    [FMatchCount, TMSFNCPlanner1.Items.Count]
  );
end;</pre>

<p>Notice what isn&#39;t happening here.</p>

<p>We aren&#39;t:</p>

<ul>
<li>Iterating over the planner items ourselves.</li>
<li>Reading every <code>Title</code> property manually.</li>
<li>Rebuilding the filter expression.</li>
<li>Copying the current search value into the rule.</li>
</ul>

<p>We simply apply the existing rule again.</p>

<p>The Rules Manager resolves <code>{Search}</code> from <code>SearchEdit.Text</code>, iterates over the Planner items, evaluates their <code>Title</code> properties and calls the appropriate action.</p><p><br></p>

<h3>What Happens When the Rule Is Disabled?</h3>

<p>Because our rule changes the appearance of the Planner items, we also need to consider what should happen when the rule itself becomes inactive.</p>

<p>For that, we can provide an <code>InactiveCallBackAction</code>.</p>

<pre class="delphi" name="code">FRule1.InactiveCallBackAction :=
  procedure(
    const ARule: TTMSFNCFilterRule;
    AItem: TObject;
    AIndex: Integer)
  var
    Item: TTMSFNCPlannerItem;
    Idx: Integer;
  begin
    Item := AItem as TTMSFNCPlannerItem;
    Idx := Item.Index;

    Item.Color := FColors[Idx].Fill;
    Item.StrokeColor := FColors[Idx].Stroke;
    Item.FontColor := FColors[Idx].Font;
    Item.TitleFontColor := FColors[Idx].Font;
    Item.MarkColor := FColors[Idx].Mark;
  end;</pre>

<p>When the rule is inactive, every appointment is restored to its original appearance.</p>

<p>This gives us three clear states:</p>

<ul>
<li><b>Match</b> - keep the original appearance.</li>
<li><b>No match</b> - dim the appointment.</li>
<li><b>Rule inactive</b> - restore everything.</li></ul><p><br></p><ul>
</ul>

<h3>The Complete Rule Setup</h3>

<p>If we remove the supporting UI and styling code, the actual rule setup is surprisingly compact:</p>

<pre class="delphi" name="code">FRM := TTMSFNCFilterRulesManager.Create(Self);

FRM.AddPlaceholder(
  &#39;Search&#39;,
  SearchEdit,
  &#39;Text&#39;
);

FRule1 := FRM.AddRule(
  &#39;SpotlightRule&#39;,
  TMSFNCPlanner1,
  &#39;Items&#39;,
  &#39;[Title] LIKE &#39;&#39;%{Search}%&#39;&#39;&#39;
);

FRule1.CallBackAction :=
  procedure(
    const ARule: TTMSFNCFilterRule;
    AItem: TObject;
    AIndex: Integer;
    const AMatched: Boolean)
  begin
    // Apply the matching or non-matching appearance here.
  end;</pre>

<p>And when the search changes:</p>

<pre class="delphi" name="code">FRM.ApplyRule(FRule1);</pre>

<p>That&#39;s the core of the example.</p><p><br></p>

<h3>Not Just a Planner Feature</h3>

<p>Although we&#39;re using <b>TTMSFNCPlanner</b> here, there is nothing Planner-specific about the rule itself.</p>

<p>The important ingredients are:</p>

<ul>
<li>An object that acts as the source.</li>
<li>A path to the objects you want to evaluate.</li>
<li>A property on those objects.</li>
<li>A filter expression.</li>
<li>An action to perform with the result.</li>
</ul>

<p>In this example:</p>

<pre class="delphi" name="code">Source      : TMSFNCPlanner1
Items       : Items
Property    : Title
Condition   : LIKE &#39;%{Search}%&#39;
Action      : Highlight matching appointments</pre>

<p>Change the source, item path and property, and the same approach can be used elsewhere in your application.</p>

<p>You could evaluate properties on visual controls, items in another FNC component, or your own Delphi objects.</p><p><br></p>

<h3>Filtering Without Removing Anything</h3>

<p>This example also shows why the term <i>filtering</i> becomes broader with the Filter Rules Manager.</p>

<p>Traditionally, filtering means that something either remains visible or disappears from the result.</p>

<p>Here, every appointment remains in the Planner.</p>

<p>The filter is simply being used to determine which rule should be applied to each item.</p>

<p>That result could change appearance, visibility, enabled state or another property. Or, as we&#39;ve done here, it can be passed to custom application logic through a callback.</p>

<p>And because the condition can work directly with object properties such as <code>Title</code>, this isn&#39;t limited to traditional data filtering.</p>

<p>That&#39;s where <b>TTMSFNCFilterRulesManager</b> starts to become particularly useful: the filtering engine becomes a way to describe conditions throughout your application, while rules determine what should happen when those conditions are met.</p>
```

		
		
		
		
		<br><br></P>]]></description>
	</item>
	</channel></rss>
