hi, i added some missing wrappers for functions in the Device Context API domain that were introduced with windows 2000 and win98.
The following functions were added in [r5003]:
The following were added in [r5024]:
Note that this revision includes an implementation of [feature-requests:#79] (GradientFill and AlphaBlend), with accompanying convenience functions GradientTriangleFill and GradientRectFill.
Commit: [r5003]
Commit: [r5024]
Discussion: Selecting and restoring objects in TDC
Feature Requests: #79
Wiki: OWLNext_Roadmap_and_Prereleases
Anonymous
and i have a question there: how much c++ is wanted in those wrappers?
for example a wrapper for this API:
https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-gettextextentexpointi
there are 4 pointers that can possibly go wrong and 4 basic usecases i can imagine:
usecase 1: we give in a list of glyphs and only want the bounding box - here we could replace
lpwszString+cwchStringwith astd::vector<WORD>to minimize the parametercount and improve security:usecase 2: we give in a list of glyphs and we want the range of glyphs that could fit into some range of logical units. we omit the uninteresting pointer
lpnDxfrom the declaration:
case 3: maybe we want it even more easy than that and allow the vector to shrink to the size of only fitting glyphs?
usecase 4:
we are really interested not only in the amount of fitting glyphs, but in unit delta positions of the glyphs:
this way we could resize the partitialGlyphs ourselfes to ensure that the buffer cant possibly be to small...
where does the abstraction end? :D
Last edit: Chris Kohlert 2020-05-20
Hi Chris, great work! Good of you to include documentation as well. I didn't know these functions even existed.
Quick review:
Now, some comments on API design:
The 7 series wants lots! :-)
It sounds like all your overloads make sense, since the underlying function has so much multi-functionality. But don't return an empty TSize to signal error. If that should not happen, i.e. it is an exceptional error, throw an exception instead. If the client code does not want exceptions, and want to do explicit error checking (which it probably won't anyway), it can use the raw C-style overload.
Looking at this issue more generally, the OWL tradition is to provide a very thin encapsulation, mainly just wrapping an underlying handle. Then we've added more overloads that makes usage simpler and safer, such as the overloads for standard C++ strings, and sometimes we've added utility types to deal with complex parameters (the Windows API has a propensity for multi-functionality functions with large and complex parameter pack structure types).
While the thin wrapping makes little difference in usage (e.g.
dc.EndPath()vsEndPath(dc)), it does allow discoverability with modern code editors, i.e. you press period and the list of member functions to choose from pops up. That's good. Having the member function also match the Windows API name and function parameters more or less 1-to-1 is also good for understanding the implementation and cross-referencing the Windows API documentation. In other words, there is no obscuring encapsulation. If you understand the Windows API, you understand OWL.However, there are problems with exposing the Windows API. First and foremost, it is C. It is not C++. So it has all the C pitfalls with pointers, buffers and (overlooked) error return codes.
For example, take that simple EndPath. Should TDC::EndPath return the Windows API error code, or should it throw an exception on error? It currently returns the error code (bool), but I bet it is rarely checked in most client code. That's bad.
Get-functions in the Windows API usually return the result in an out-parameter via pointer, while the function itself returns an error code. For example, TDC::GetClipBox faithfully replicates this API, but changes the rectangle parameter from pointer to reference (since passing nullptr has no meaning for the underlying function). That's good. It also provides an overload TDC::GetClipBox () which returns the rectangle as the return value. That's even better. It is more user-friendly and composable, for less verbose and more robust C++ code (however, note that the implementation ignores the error code returned by ::GetClipBox — it should throw an exception!).
I guess we could use some formal guidelines on this. For now, these are my rules-of-thumb from the top of my head:
For a really complex example of encapsulating the Windows API, see my latest work on TRegKey::GetSecurity. It has the thin wrapper overload complemented by a C++ style overload of the same name, TRegKey::GetSecurity (SECURITY_INFORMATION), returning the utility type TSecurityDescriptor, which in turn encapsulates the SECURITY_DESCRIPTOR type and associated functions in the Windows API.
Last edit: Vidar Hasfjord 2020-05-19
Also, see open ticket [feature-requests:#79] "Add TDC::GradientFill and TDC::AlphaBlend". It can be wrapped into this ticket. Go ahead! Take ownership! :-)
Related
Feature Requests: #79
did that. :) i will work on it :D
Hi
:D :D i guess you are right, sorry i will fix that xD
GetAttributeHDC() and GetHDC() do the same thing, i didnt know when to use which. because GetAttributeHDC() is virtual and seem to be used with some printer-related functions, i thought i use it for getters rather than setters?!
the overload GetTextExtentPointI with the ignoring error-return in the patch is analog to other functions i found in the same spot for exact this purpose (ignoring the optional error result) - but i will change that to an exception in case of an error.
thats why i used the original API-Type (LPSOMETHING) of that function.
i hoped that would make the purpose of using a buffer here clear :)
what would you do? use
WORD glyphArray[]instead ofLPWORD glyphArray?in case of the parameter-names thats not always a good idea because in the API often those names are cryptic and sometimes plain wrongly named (in this example the parameter that i named "glyphExtents" is original named "lpnDx" and the documentation is very misleading in what the array that it points to holds at the end).
i would vote that at least the parameter index should be the same as in the API for a thin layer.
another point in the wrapper is: is it a good idea to change the size of the vectors like i did above? should one do that this way - even for safety reasons - or is it too unexpected for the caller?
Last edit: Chris Kohlert 2020-05-20
I am confused, too. But I think it is important to get it right, because, as you point out, it is used in the printer preview functionality, in which a preview DC encapsulates a scaled DC (the previewed page).
Aye. The TDC code is poor. I am studying it now. It lacks error handling in many places, const-correctness is poor (conceptual rather than actual, and sometimes inconsistent, as you noticed), and functions return the wrong return type sometimes also (bool instead of code). I will post some bug tickets soon. In Owlet, I may try a partial rewrite with consistent exception handling, which I will post later for feedback. Hopefully, we can set a better direction for the standard of this code.
I was referring to this:
Here, by conventional coding standards, glypharray is not a buffer. It is a reference to a single WORD. Treating it as the start of a buffer is undefined behaviour. Use a conventional buffer pointer and size.
Fully agree. No Hungarian notation, please!
Instinctively, I don't like out-parameters and in-out-parameters. Instead, I prefer to pass values and return values in pure functional style. If you need to return more than one value, create a structure (or use std::pair or std::tuple, if that is not too confusing).
That said, perhaps you shouldn't hard code vectors here at all. The user may want to use a different container type (C-array, std::array, dynamic allocation, etc.). Buffer pointer plus buffer size is flexible and conventional. If you want something neater, go for a generic contiguous range perhaps:
For return values, you can, and should, return a vector where it makes sense.
In this case, there is only the vector to return, but for the more complex functions, you may need to package it into a struct with the other data returned. In any case, don't worry about returning vectors being inefficient; move semantics, C++17 copy elision and optimizers take care of that.
Last edit: Vidar Hasfjord 2020-05-20
Okay, thanks, i try to keep all of that in mind :D
... just whow. in that case good documentation is important because a user of this function couldnt see quickly that this is basically "some container of WORDs" ... xD
Last edit: Chris Kohlert 2020-05-20
True. Although usage is simple in this case, templates can quickly become confusing (took me a while to get that static_assert right). But Doxygen is our friend. And in short order, we should assume the user is well versed in ranges and that C++20 is old-school. Time flies! :-)
PS. I've fixed my code posted earlier, correcting naming ("bounding box" to "extent"), and added an example of a GetTextExtentExPointI implementation returning the partial extents vector as well in a utility structure.
Thinking a bit about my implementation of GetTextExtentExPointI: While returning a vector is pretty efficient with modern C++, creating one over and over requires repeated dynamic allocation. So if you need to process a lot of strings, my posted solution isn't very efficient. However, it is pretty easy to fix this with move semantics and another optional parameter that lets the user pass in a vector for reuse. I think this should work, without requiring dynamic allocation in every call, except when required to grow the vector.
Like I said, we want lots of C++ in the 7 series! :-)
Last edit: Vidar Hasfjord 2020-05-20
okay, nice, but why is:
auto fitCount = int{};so much better than
int fitCount;? :D
(i need some moments to fully understand what you did there xD)
okay, i think this is a good solution, even if its on a c++ heavy side :D
Last edit: Chris Kohlert 2020-05-20
I am preparing for the day the latter is syntax error.
And it is more C++! :-)
(Seriously, check out Herb Sutter's talk on this; "Almost Always Auto style").
Good to hear! I just wrote a little test program to reassure myself that TTextExtent got the move semantics by default that I was expecting. Seeing C++ winding its way through function call, value return and repeated assignment without a copy in sight was lovely to behold. :-)
Last edit: Vidar Hasfjord 2020-05-20
Let me elaborate upon that a little. It is about expressivity. Also, I have a simpler, safer and much more regular C++ language in the back of my head when I write code.
I was slightly flippant when I said I was preparing for the C style to become a syntax error. But actually that wouldn't be all bad. The convoluted declaration syntax and rules inherited from C complicates the C++ language standard a lot. Not that I have read it in detail, but the variable
iexists in some kind of strange hinterland after the statementint i;. A variable name has been reserved, but no object yet exists.So, with my
auto i = int{};, I brought an unfortunate situation in the code to your attention: I needed to reserve a variable before I could define its value (this is a problem with ugly APIs using out-parameters). Although less verbose, I didn't sayauto i = 0;, because 0 is not important, and I didn't sayint i = 0;, because that is superfluous type information (the compiler knows that 0 is an int), and I follow the rule that the important type information is on the right-hand side (AAA rule).Last edit: Vidar Hasfjord 2020-05-20
To allow an initializer list to be used with GetTextExtentExPointI without explicit type qualification, i.e.
GetTextExtentExPointI({...}, 42)rather than the rather long-windedGetTextExtentExPointI(std::initializer_list<WORD>{...}, 42), add this resolving overload, which simply forwards to the templated overload:Last edit: Vidar Hasfjord 2020-05-21
maybe you should look into Rust a bit, i have the feeling you would like it ;)
https://doc.rust-lang.org/book/
your "def", "var" approach is pretty much how its done in Rust if i remember correctly :D
Last edit: Chris Kohlert 2020-05-21
can i wrap LPWORD + size into some type in c++ so that is is compatible to work as TContiguousRangeOfWord in your GetTextExtentExPointI example?
i mean - with out-of-the-box utilities? i cant find anything right now :D
Last edit: Chris Kohlert 2020-05-21
okay, i found it, initializer_list works even in this case just fine :D
https://godbolt.org/z/nM4cAk
Rust is interesting. I have had it on my todo-list for a long time. I followed the D language development closely a while back — another system language contender. I need to check up on the progress there, also.
And interesting that initializer_list is useful to "range-ify" any contiguous sequence of element data. I have not thought of that. Thanks for sharing.
By the way, good to see another Compiler Explorer user here. That is an extraordinary cool tool. The author Godbolt does interesting presentations too.
Regarding the optimisation we've explored here, it should be applied to other functions as well, of course, such as GetGlyphIndices, for which I offered a possible implementation earlier.
With this in place, it made me think that we now have a solution that may be able to beat the performance of Windows' own GetTextExtentPoint32 (encapsulated by TDC::GetTextExtentPoint) and GetTextExtentExPoint (not yet encapsulated). Internally, I assume that those functions are implemented by converting the string to a glyph array and then processing it by forwarding to one of GetTextExtentPointI or GetTextExtentExPointI. That, I guess, may very well involve dynamic allocation for the glyph array (unless Windows does some clever buffer reuse). We can eliminate that by doing the glyph conversion in a separate step while reusing our vectors:
Last edit: Vidar Hasfjord 2020-05-21
By the way, do you know how in Doxygen you link to a specific function overload, when that overload is a function template?
hmm, does
not work? - i cant test it right now
Last edit: Chris Kohlert 2020-05-21
didnt work either :(
Last edit: Chris Kohlert 2020-05-21