Suppose I have a GDI+ GraphicsPath that is relatively complex, with "holes" in it. Text is a good example, like the letter "O". I want to transform this path so that I can fill it in completely, including the "holes." How can I do this?
Cody,
I didn't see that you had accepted an answer yet, so I am putting this C# function here for you to see if it helps. It has been tested.
Slightly different from the version above: This routine looks for the path with the largest bounding area, so it is a bit more generous than the version above since it doesn't require the "master" path to pass 2 tests to prove that it is worthy of keeping.
I made this into an extension method, so in .Net you can just write:
GraphicsPath solid = LetterPath.ToSolidPath();
The return value is a new GraphicsPath whose interior has been eviscerated (wow, I don't get to use that word very often).
/// <summary>
/// Removes all subpaths (holes) from a graphics path, leaving only the largest whole path behind
/// </summary>
public static GraphicsPath ToSolidPath(this GraphicsPath path)
{
GraphicsPath BiggestPath = null;
GraphicsPath SubPath = new GraphicsPath();
RectangleF BoundsRect = RectangleF.Empty;
RectangleF BiggestRect = RectangleF.Empty;
bool bIsClosed = false;
var pathIterator = new GraphicsPathIterator(path);
pathIterator.Rewind();
for (int i = 0; i < pathIterator.SubpathCount; i++)
{
SubPath.Reset();
pathIterator.NextSubpath(SubPath, out bIsClosed);
BoundsRect = SubPath.GetBounds();
if (BoundsRect.Width * BoundsRect.Height > BiggestRect.Width * BiggestRect.Height)
{
BiggestRect = BoundsRect;
BiggestPath = (GraphicsPath)SubPath.Clone();
}
}
return BiggestPath;
}
Here's a partial solution, in Delphi, that I came up with. It only works in cases where the entire path is "contained" within a single subpath. It simply iterates the subpaths and returns a new path identical to the biggest subpath. It is not a universal solution to the problem above, but it works for the case at hand, and might help someone else in the future:
function BlockPath(Path: IGPGraphicsPath): IGPGraphicsPath;
var
PathIterator: IGPGraphicsPathIterator;
SubPath: IGPGraphicsPath;
I: Integer;
IsClosed: Boolean;
BiggestPath: IGPGraphicsPath;
BiggestRect, BoundsRect: TGPRectF;
begin
Result := TGPGraphicsPath.Create;
SubPath := TGPGraphicsPath.Create;
PathIterator := TGPGraphicsPathIterator.Create(Path);
PathIterator.Rewind;
BiggestPath := nil;
BiggestRect.Width := 0;
BiggestRect.Height := 0;
for I := 0 to PathIterator.SubpathCount - 1 do
begin
SubPath.Reset;
PathIterator.NextSubPath(SubPath, IsClosed);
SubPath.GetBounds(BoundsRect);
if (BoundsRect.Width >= BiggestRect.Width) and
(BoundsRect.Height >= BiggestRect.Height) then
begin
BiggestRect := BoundsRect;
BiggestPath := SubPath.Clone;
end;
end;
if BiggestPath <> nil then
begin
Result.AddPath(BiggestPath, True);
end;
end;
Comments and improvements are welcome.
Related
I am working on a compiler and one aspect currently is how to wait for interpolated variable names to be resolved. So I am wondering how to take a nested interpolated variable string and build some sort of simple data model/schema for unwrapping the evaluated string so to speak. Let me demonstrate.
Say we have a string like this:
foo{a{x}-{y}}-{baz{one}-{two}}-foo{c}
That has 1, 2, and 3 levels of nested interpolations in it. So essentially it should resolve something like this:
wait for x, y, one, two, and c to resolve.
when both x and y resolve, then resolve a{x}-{y} immediately.
when both one and two resolve, resolve baz{one}-{two}.
when a{x}-{y}, baz{one}-{two}, and c all resolve, then finally resolve the whole expression.
I am shaky on my understanding of the logic flow for handling something like this, wondering if you could help solidify/clarify the general algorithm (high level pseudocode or something like that). Mainly just looking for how I would structure the data model and algorithm so as to progressively evaluate when the pieces are ready.
I'm starting out trying and it's not clear what to do next:
{
dependencies: [
{
path: [x]
},
{
path: [y]
}
],
parent: {
dependency: a{x}-{y} // interpolated term
parent: {
dependencies: [
{
}
]
}
}
}
Some sort of tree is probably necessary, but I am having trouble figuring out what it might look like, wondering if you could shed some light on that with some pseudocode (or JavaScript even).
watch the leaf nodes at first
then, when the children of a node are completed, propagate upward to resolving the next parent node. This would mean once x and y are done, it could resolve a{x}-{y}, but then wait until the other nodes are ready before doing the final top-level evaluation.
You can just simulate it by sending "events" to the system theoretically, like:
ready('y')
ready('c')
ready('x')
ready('a{x}-{y}')
function ready(variable) {
if ()
}
...actually that may not work, not sure how to handle the interpolated nodes in a hacky way like that. But even a high level description of how to solve this would be helpful.
export type SiteDependencyObserverParentType = {
observer: SiteDependencyObserverType
remaining: number
}
export type SiteDependencyObserverType = {
children: Array<SiteDependencyObserverType>
node: LinkNodeType
parent?: SiteDependencyObserverParentType
path: Array<string>
}
(What I'm currently thinking, some TypeScript)
Here is an approach in JavaScript:
Parse the input string to create a Node instance for each {} term, and create parent-child dependencies between the nodes.
Collect the leaf Nodes of this tree as the tree is being constructed: group these leaf nodes by their identifier. Note that the same identifier could occur multiple times in the input string, leading to multiple Nodes. If a variable x is resolved, then all Nodes with that name (the group) will be resolved.
Each node has a resolve method to set its final value
Each node has a notify method that any of its child nodes can call in order to notify it that the child has been resolved with a value. This may (or may not yet) lead to a cascading call of resolve.
In a demo, a timer is set up that at every tick will resolve a randomly picked variable to some number
I think that in your example, foo, and a might be functions that need to be called, but I didn't elaborate on that, and just considered them as literal text that does not need further treatment. It should not be difficult to extend the algorithm with such function-calling features.
class Node {
constructor(parent) {
this.source = ""; // The slice of the input string that maps to this node
this.texts = []; // Literal text that's not part of interpolation
this.children = []; // Node instances corresponding to interpolation
this.parent = parent; // Link to parent that should get notified when this node resolves
this.value = undefined; // Not yet resolved
}
isResolved() {
return this.value !== undefined;
}
resolve(value) {
if (this.isResolved()) return; // A node is not allowed to resolve twice: ignore
console.log(`Resolving "${this.source}" to "${value}"`);
this.value = value;
if (this.parent) this.parent.notify();
}
notify() {
// Check if all dependencies have been resolved
let value = "";
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
if (!child.isResolved()) { // Not ready yet
console.log(`"${this.source}" is getting notified, but not all dependecies are ready yet`);
return;
}
value += this.texts[i] + child.value;
}
console.log(`"${this.source}" is getting notified, and all dependecies are ready:`);
this.resolve(value + this.texts.at(-1));
}
}
function makeTree(s) {
const leaves = {}; // nodes keyed by atomic names (like "x" "y" in the example)
const tokens = s.split(/([{}])/);
let i = 0; // Index in s
function dfs(parent=null) {
const node = new Node(parent);
const start = i;
while (tokens.length) {
const token = tokens.shift();
i += token.length;
if (token == "}") break;
if (token == "{") {
node.children.push(dfs(node));
} else {
node.texts.push(token);
}
}
node.source = s.slice(start, i - (tokens.length ? 1 : 0));
if (node.children.length == 0) { // It's a leaf
const label = node.texts[0];
leaves[label] ??= []; // Define as empty array if not yet defined
leaves[label].push(node);
}
return node;
}
dfs();
return leaves;
}
// ------------------- DEMO --------------------
let s = "foo{a{x}-{y}}-{baz{one}-{two}}-foo{c}";
const leaves = makeTree(s);
// Create a random order in which to resolve the atomic variables:
function shuffle(array) {
for (var i = array.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
[array[j], array[i]] = [array[i], array[j]];
}
return array;
}
const names = shuffle(Object.keys(leaves));
// Use a timer to resolve the variables one by one in the given random order
let index = 0;
function resolveRandomVariable() {
if (index >= names.length) return; // all done
console.log("\n---------------- timer tick --------------");
const name = names[index++];
console.log(`Variable ${name} gets a value: "${index}". Calling resolve() on the connected node instance(s):`);
for (const node of leaves[name]) node.resolve(index);
setTimeout(resolveRandomVariable, 1000);
}
setTimeout(resolveRandomVariable, 1000);
your idea of building a dependency tree it's really likeable.
Anyway I tryed to find a solution as simplest possible.
Even if it already works, there are many optimizations possible, take this just as proof of concept.
The background idea it's produce a List of Strings which you can read in order where each element it's what you need to solve progressively. Each element might be mandatory to solve something that come next in the List, hence for the overall expression. Once you solved all the chunks you have all pieces to solve your original expression.
It's written in Java, I hope it's understandable.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public class StackOverflow {
public static void main(String[] args) {
String exp = "foo{a{x}-{y}}-{baz{one}-{two}}-foo{c}";
List<String> chunks = expToChunks(exp);
//it just reverse the order of the list
Collections.reverse(chunks);
System.out.println(chunks);
//output -> [c, two, one, baz{one}-{two}, y, x, a{x}-{y}]
}
public static List<String> expToChunks(String exp) {
List<String> chunks = new ArrayList<>();
//this first piece just find the first inner open parenthesys and its relative close parenthesys
int begin = exp.indexOf("{") + 1;
int numberOfParenthesys = 1;
int end = -1;
for(int i = begin; i < exp.length(); i++) {
char c = exp.charAt(i);
if (c == '{') numberOfParenthesys ++;
if (c == '}') numberOfParenthesys --;
if (numberOfParenthesys == 0) {
end = i;
break;
}
}
//this if put an end to recursive calls
if(begin > 0 && begin < exp.length() && end > 0) {
//add the chunk to the final list
String substring = exp.substring(begin, end);
chunks.add(substring);
//remove from the starting expression the already considered chunk
String newExp = exp.replace("{" + substring + "}", "");
//recursive call for inner element on the chunk found
chunks.addAll(Objects.requireNonNull(expToChunks(substring)));
//calculate other chunks on the remained expression
chunks.addAll(Objects.requireNonNull(expToChunks(newExp)));
}
return chunks;
}
}
Some details on the code:
The following piece find the begin and the end index of the first outer chunk of expression. The background idea is: in a valid expression the number of open parenthesys must be equal to the number of closing parenthesys. The count of open(+1) and close(-1) parenthesys can't ever be negative.
So using that simple loop once I find the count of parenthesys to be 0, I also found the first chunk of the expression.
int begin = exp.indexOf("{") + 1;
int numberOfParenthesys = 1;
int end = -1;
for(int i = begin; i < exp.length(); i++) {
char c = exp.charAt(i);
if (c == '{') numberOfParenthesys ++;
if (c == '}') numberOfParenthesys --;
if (numberOfParenthesys == 0) {
end = i;
break;
}
}
The if condition provide validation on the begin and end indexes and stop the recursive call when no more chunks can be found on the remained expression.
if(begin > 0 && begin < exp.length() && end > 0) {
...
}
I've been trying to write values to memory bits if certain conditions are verified, but so far not very successfully. In the piece of code presented below I have 3 variables which I've set to 1, therefore the condition returns the value TRUE (I've tested this). However, the values of variables "M_ALARME_BENCH_STOP" and "M_ALARME_BENCH_WARN" (memory bits) do not comute to the desired values. What am I doing wrong/missing?
IF ((S_DHW_IN_TP = 1) AND (IU_DHW_IN_TP = 1) AND (C_DHW_IN_TP = 1)) THEN
M_ALARME_BENCH_STOP := TRUE;
M_ALARME_BENCH_WARN := FALSE;
ELSIF ((S_DHW_IN_TP = 0) AND (IU_DHW_IN_TP = 1) AND (C_DHW_IN_TP = 1)) THEN
M_ALARME_BENCH_WARN := TRUE;
M_ALARME_BENCH_STOP := FALSE;
ELSE
M_ALARME_BENCH_WARN := FALSE;
M_ALARME_BENCH_STOP := FALSE;
END_IF
The piece of code you provided should work as you described. Are you sure, that these 2 variables are not being set somewhere else in your code after this IF statement?
You can check it by placing a breakpoint directly after END_IF. If they are set to desired values on the breakpoint, then they are probably reset somewhere else.
Enums are/will be the Option replacements for Business Central 365. Recently I had an occasion to use a few and get my feet wet, so to speak. As seems to be the case far too often, about 80% of the functionality you need is readily available, but the remaining 20% takes more work than it should.
In the case of Enums, you get a Text list of Names and an Integer list of associated Ordinal values, but you do NOT get a list of Captions. In the partial example Enum FourStates below, Default, OH and TX are the Names, 0, 1 and 2 are the Ordinals and space, Ohio and Texas are the captions. Note that Ordinals are the defined numeric values, NOT the indexes. Perfectly valid Ordinals in the example below could be 1, 5 and 7.
value(0; Default) { Caption = ' '; }
value(1; OH) { Caption = 'Ohio'; }
value(2; TX) { Caption = 'Texas'; }
If you define a Table or Page field as an Enum, then the captions are displayed in the dropdowns. To get the Caption you can use Format(EnumType::Name) but we needed to iterate all of the Captions for a given Enum.
After digging around in some blogs and the documentation, here is a summary of what I found.
First, there is a major limitation because Captions can ONLY be processed within the context of an Enum type and, at least as of now, the Business Central 365 SaaS solution has no support for generic Enums (e.g. EnumRef like RecordRef and FieldRef). This means a Text list of Captions must be created on an Enum by Enum basis.
However, once the Captions list has been created, then using the combination of Name, Ordinal and Caption Lists of [Text], you can then write general purpose code that will work for any Enum.
A Gotcha, when you use the TEnum code snippet in VS Code, it defaults the first element to 0. We have learned to either make that the Default as the partial code above does, or change it to 1. The reason for this is because if there is any chance that you will ever use an Enum with the StrMenu command, StrMenu defaults 0 as the Cancel return value. In that case, you could never select the 0 Ordinal Enum because ti would be treated as Canceled.
To create an instance of an Enum to use within your code, create a Var variable similar to MyChoices: Enum "Defined Choices";. If you also define a MyChoice: Enum: "Defined Choices", you can compare them with something like if MyChoice = MyChoices::FirstChoice then, etc.
Below is some sample code with a few different Enum styles along with a method that allows you to create a List of [Text] for the Captions. Again, that has to be coded for each specific Enum.
a. Within VS Code, use AL Go to create a new HelloWorld app
b. Change the app.json file Name and Publisher, I named mine EnumHelper
NOTE: We always define a Publisher because you cannot filter for Default Publisher within SaaS
c. Replace all of the code inside the HelloWorld.al file with the code below
NOTE: To simplify things, everything below is all in the same file
d. The code is a PageExtension for the Chart of Accounts Page and runs off the OnOpenPage trigger
This allows the code to be easily called without requiring a bunch of setup code.
Here is the code that allows you to create a Captions list. The variable myOrdinal is an Integer and Flagged is a defined Enum. Note the Enum::EnumName, similar to Page::PageName or Database::TableName.
foreach myOrdinal in Flagged.Ordinals do begin
// Enum definition, NOT an enum instance.
captions.Add(Format(Enum::Flagged.FromInteger(myOrdinal)));
end;
All of the code (sorry, it didn't format exactly correctly)
enum 50200 FourStates
{
Extensible = true;
value(0; Default) { Caption = ' '; }
value(1; OH) { Caption = 'Ohio'; }
value(2; TX) { Caption = 'Texas'; }
value(3; NC) { Caption = 'North Carolina'; }
value(4; IA) { Caption = 'Iowa'; }
value(5; MO) { Caption = 'Missouri'; }
}
enum 50201 Flagged
{
Extensible = true;
value(0; Default) { Caption = ' '; }
value(1; Bold) { Caption = 'Bold'; }
value(2; ITalic) { Caption = 'Italid '; }
value(4; Underline) { Caption = 'Underline'; }
value(8; BoldItalic) { Caption = 'Bold & Italic'; }
value(16; BoldUnderline) { Caption = 'Bold & Underline '; }
value(32; ItalicUnderline) { Caption = 'Italic & Underline'; }
value(64; All3Options) { Caption = 'All 3 Options'; }
}
enum 50202 Randomized
{
Extensible = true;
value(0; Default) { Caption = ' '; }
value(7; Good) { Caption = 'The Good'; }
value(5; Bad) { Caption = 'The Bad'; }
value(11; Ugly) { Caption = 'The Ugly'; }
}
enum 50203 ProcessFlowOptions
{
Extensible = true;
value(0; Default) { Caption = ' '; }
value(1; Flagged) { Caption = 'Flagged'; }
value(2; Randomized) { Caption = 'Randomized'; }
value(4; FourStates) { Caption = 'FourStates'; }
}
pageextension 50200 "Chart of Accounts EH" extends "Chart of Accounts"
{
var
// Enum instance variables.
myFlagged: Enum Flagged;
myFourStates: Enum FourStates;
myRandomized: Enum Randomized;
trigger OnOpenPage();
begin
case UserID.ToLower() of
'larry':
Message('Hello Larry, this is an extension for October testing.');
'vicki':
Message('Good morning Vicki, this is an extension for October testing.');
else
if Confirm('Hello %1 from EnumHelper.\\Click Yes to process or no to cancel.', true, UserID) then begin
ProcessEnumerations();
end;
end;
end;
local procedure ProcessEnumerations()
var
allLines: TextBuilder;
randomCaptions: List of [Text];
flaggedCaptions: List of [Text];
fourStatesCaptions: List of [Text];
begin
GetEnumCaptions(randomCaptions, flaggedCaptions, fourStatesCaptions);
IterateEnumNamesOrdinalsAndCaptions(allLines, randomCaptions, flaggedCaptions, fourStatesCaptions);
Message(allLines.ToText());
end;
local procedure GetEnumCaptions(randomCaptions: List of [Text]; flaggedCaptions: List of [Text]; fourStatesCaptions: List of [Text])
begin
GetCaptions(randomCaptions, ProcessFlowOptions::Randomized);
GetCaptions(flaggedCaptions, ProcessFlowOptions::Flagged);
GetCaptions(fourStatesCaptions, ProcessFlowOptions::FourStates);
end;
local procedure IterateEnumNamesOrdinalsAndCaptions(allLines: TextBuilder; randomCaptions: List of [Text]; flaggedCaptions: List of [Text]; fourStatesCaptions: List of [Text])
begin
IterateEnumNamesOrdinalsAndCaptions('Flagged Enum', allLines, myFlagged.Names, myFlagged.Ordinals, flaggedCaptions);
IterateEnumNamesOrdinalsAndCaptions('Randomized Enum', allLines, myRandomized.Names, myRandomized.Ordinals, randomCaptions);
IterateEnumNamesOrdinalsAndCaptions('FourStates Enum', allLines, myFourStates.Names, myFourStates.Ordinals, fourStatesCaptions);
end;
local procedure IterateEnumNamesOrdinalsAndCaptions(title: Text; allLines: TextBuilder; enumNames: List of [Text]; enumOrdinals: List of [Integer]; enumCaptions: List of [Text])
var
i: Integer;
enumLine: TextBuilder;
enumLines: TextBuilder;
begin
allLines.AppendLine(title);
allLines.appendLine();
for i := 1 to enumNames.Count do begin
Clear(enumLine);
enumLine.AppendLine('EnumName: ''' + enumNames.Get(i) + ''',');
enumLine.AppendLine('EnumOrdinal: ' + Format(enumOrdinals.Get(i)) + ',');
enumLine.AppendLine('EnumCaption: ''' + enumCaptions.Get(i) + '''.');
//enumLine.AppendLine('EnumName: ''' + enumNames.Get(i) + ''', EnumOrdinal: ' + ordinal + ', EnumCaption: ''' + enumCaptions.Get(i) + '''');
enumLines.AppendLine(enumLine.ToText());
end;
allLines.AppendLine(enumLines.ToText());
end;
local procedure GetCaptions(captions: List of [Text]; processFlowOption: Enum ProcessFlowOptions)
var
myOrdinal: Integer;
myProcessFlowOptions: Enum ProcessFlowOptions;
begin
// Load captions by iterating specific Enums.
case processFlowOption of
myProcessFlowOptions::Flagged:
begin
foreach myOrdinal in Flagged.Ordinals do begin
// Enum definition, NOT an enum instance.
captions.Add(Format(Enum::Flagged.FromInteger(myOrdinal)));
end;
end;
myProcessFlowOptions::Randomized:
begin
foreach myOrdinal in Randomized.Ordinals do begin
// Enum definition, NOT an enum instance.
captions.Add(Format(Enum::Randomized.FromInteger(myOrdinal)));
end;
end;
myProcessFlowOptions::FourStates:
begin
foreach myOrdinal in FourStates.Ordinals do begin
// Enum definition, NOT an enum instance.
captions.Add(Format(Enum::FourStates.FromInteger(myOrdinal)));
end;
end;
end;
end;
}
Enjoy
I have implemented a better workaround for BC14. That should work on newer versions also, but I have tested on BC14 only.
var
RecRef: RecordRef;
FRef: FieldRef;
OptionNames: List of [Text];
OptionCaptions: List of [Text];
i: Integer;
RecRef.GetTable(Rec); // Some record
FRef := RecRef.Field(20); // Some field of type Enum
OptionNames := FRef.OptionMembers().Split(',');
OptionCaptions := FRef.OptionCaption().Split(',');
for i := 1 to OptionNames.Count() do begin
Evaluate(FRef, OptionNames.Get(i));
Message('Ordinal = %1\Name = %2\Caption = %3',
format(FRef, 0, 9),
OptionNames.Get(i),
format(FRef, 0, 1)); // or OptionCaptions.Get(i)
end;
I am trying to use the GetConsoleScreenBufferInfo(HANDLE, PCONSOLE_SCREEN_BUFFER_INFO) function from the Windows API using Perl 6 and (of course) NativeCall.
I think I have set up the CONSOLE_SCREEN_BUFFER_INFO struct the function needs correctly, but the code crashes after the call when I try to dump its content.
This is the shortest (not quite but close) way to demonstrate the problem:
use NativeCall;
constant \HANDLE := Pointer[void];
constant \SHORT := int16;
constant \USHORT := uint16;
constant \WORD := uint16;
constant \DWORD := uint32;
constant \BOOL := int32;
constant \STD_OUTPUT_HANDLE := -11;
constant \STD_INPUT_HANDLE := -10;
class COORD is repr('CStruct') {
has SHORT $.X;
has SHORT $.Y;
}
class SMALL_RECT is repr("CStruct") {
has SHORT $.Left;
has SHORT $.Top;
has SHORT $.Right;
has SHORT $.Bottom;
};
class CONSOLE_SCREEN_BUFFER_INFO is repr("CStruct") {
has COORD $.dwSize;
has COORD $.dwCursorPosition;
has WORD $.wAttributes;
has SMALL_RECT $.srWindow;
has COORD $.dwMaximumWindowSize;
submethod TWEAK {
$!dwSize := COORD.new;
$!dwCursorPosition := COORD.new;
$!srWindow := SMALL_RECT.new;
$!dwMaximumWindowSize := COORD.new;
}
}
# C: BOOL WINAPI GetConsoleScreenBufferInfo(_In_ HANDLE hConsoleOutput, _Out_ PCONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo);
sub GetConsoleScreenBufferInfo(HANDLE, CONSOLE_SCREEN_BUFFER_INFO is rw) is native("Kernel32.dll") returns BOOL { * };
sub GetStdHandle(DWORD) is native('Kernel32') returns Pointer[void] { * };
my CONSOLE_SCREEN_BUFFER_INFO
$info = CONSOLE_SCREEN_BUFFER_INFO.new;
my HANDLE
$handle-o = GetStdHandle( STD_OUTPUT_HANDLE );
dd $info;
say "GetConsoleScreenBufferInfo ", GetConsoleScreenBufferInfo( $handle-o, $info );
say "Will I live?";
dd $info; #crashes without notice
Any hints as to why the crash occurs and how to fix it are very welcome.
You need to use HAS instead of has for the members of CONSOLE_SCREEN_BUFFER_INFO that are structures as these are embedded instead of referenced by pointer (which is the Perl6 default).
Once you do that, you can drop the TWEAK as well so the code will read
class CONSOLE_SCREEN_BUFFER_INFO is repr("CStruct") {
HAS COORD $.dwSize;
HAS COORD $.dwCursorPosition;
has WORD $.wAttributes;
HAS SMALL_RECT $.srWindow;
HAS COORD $.dwMaximumWindowSize;
}
In my shell extension, I want to mimic explorer's behavior and show 'This folder is empty' message when in fact my folder is empty:
However, I can't accomplish it.
Using API Monitor, I see that when explorer refreshes an empty folder, IEnumIDList::Next() is returning the following:
Meaning, that the 'next' item returned is NULL, the number of items is 0 and the result is S_FALSE.
As mentioned, I tried to mimic the return values, and indeed no items are loaded for the folder, but no message appear either.
So what API would trigger this message?
Your IEnumIDList implementation must implement IObjectWithSite. Sample of implementation:
var
ServiceProvider: IServiceProvider;
ShellBrowser: IShellBrowser;
ShellView: IShellView;
FolderView2: IFolderView2;
begin
if not Assigned(ASite) then Exit;
OleCheck(ASite.QueryInterface(IServiceProvider, ServiceProvider));
try
OleCheck(ServiceProvider.QueryService(SID_STopLevelBrowser, IShellBrowser, ShellBrowser));
try
OleCheck(ShellBrowser.QueryActiveShellView(ShellView));
try
OleCheck(ShellView.QueryInterface(IFolderView2, FolderView2));
try
FolderView2.SetText(FVST_EMPTYTEXT, 'The message you want to see');
finally
FolderView2 := nil;
end;
finally
ShellView := nil;
end;
finally
ShellBrowser := nil;
end;
finally
ServiceProvider := nil;
end;
end;
Result:
Also you can use the same code in your IShellFolder implementation.
This is an old question but I write my answer. It might be useful to someone
You can change the empty folder view text in IShellFolder::CreateViewObject method, like below:
if (riid == IID_IShellView)
{
m_hwndOwner = hwndOwner;
SFV_CREATE sfvData = { sizeof sfvData };
const CComQIPtr<IShellFolder> spFolder = GetUnknown();
sfvData.psfvcb = this;
sfvData.pshf = spFolder;
auto hr = SHCreateShellFolderView(&sfvData, reinterpret_cast<IShellView**>(ppv));
// Here we change the text
auto pSV = *reinterpret_cast<IShellView**>(ppv);
CComPtr<IFolderView2> pFV;
pSV->QueryInterface(IID_PPV_ARGS(&pFV));
pFV->SetText(FVST_EMPTYTEXT, L"Put your text here!");
//-
return hr;
}