Vielleicht interessiert es hier jemanden, so lässt sich ChatGPT auch in XProfan per API verwenden:
Quelltexte/Codesnippets/ChatGPT API in XProfan verwenden (per cURL) XProfan
Es funktioniert ab Windows 10 und XProfan 10.
Grüße,
Sven
Vielleicht interessiert es hier jemanden, so lässt sich ChatGPT auch in XProfan per API verwenden:
Quelltexte/Codesnippets/ChatGPT API in XProfan verwenden (per cURL) XProfan
Es funktioniert ab Windows 10 und XProfan 10.
Grüße,
Sven
Ich habe weder Funktionsnummern geändert noch angegeben, die Ausnahme funktioniert auch lediglich mit der einen damals eingereichten Version aber immerhin.
Ich hatte Erfolg mit der Einreichung meiner Setup-Datei bei Microsoft:
Submit a file for malware analysis - Microsoft Security Intelligence
Mein Programm wurde zwar nicht als Virus erkannt aber als unbekannter Download erstmal geblockt und der unbedarfte User sieht erstmal nur den "Nicht ausführen" Button. Das Problem war dann nach einigen Tagen behoben aber eben mit einer neuen Version der Software auch wieder da.
Ein frohes neues Jahr zusammen! Ich fürchte so einfach ist es nicht, hier etwas per downloadfile zusammenzubauen. Ich versuche mal alles außer der Base64-Kodierung zu beleuchten.
Downloadfile wird nur als sogenanntes "GET" funktionieren und ein Header lässt sich auch nicht in die URL integrieren. Es muss also wie gefordert ein POST mit entsprechendem Header verwendet werden. Ich habe dazu mal einen Code von mir modifiziert. Da ich keine Zugangsdaten habe, gibt er wie erwartet den HTTP Statuscode 401 (nicht autorisiert) zurück. Mit deinen base64 kodierten Zugansdaten dürfte es gehen allerdings geht aus deinem Beitrag noch nicht hervor wie dir eigentlichen Daten neben dem Header struktueirt sein sollen. Zum testen von APIs kann ich diese Seite sehr empfehlen: https://reqbin.com/
Sobald die Abfrage dort gelingt, weiß man, was man zu programmieren hat.
Hier der XProfan Code:
Cls RGB(40,40,40)
Def InternetOpen(6) !"wininet.dll","InternetOpenA"
Def InternetConnect(8) !"wininet.dll","InternetConnectA"
Def HttpOpenRequest(8) !"wininet.dll","HttpOpenRequestA"
Def HttpAddRequestHeaders(2) !"wininet.dll","HttpAddRequestHeadersA"
Def HttpSendRequest(5) !"wininet.dll","HttpSendRequestA"
Def InternetCloseHandle(1) !"wininet.dll","InternetCloseHandle"
Def InternetReadFile(4) !"wininet.dll","InternetReadFile"
Def HttpQueryInfo(5) !"wininet.dll","HttpQueryInfoA"
Def &INTERNET_OPEN_TYPE_PRECONFIG 0
Def &INTERNET_DEFAULT_HTTPS_PORT 443
Def &INTERNET_SERVICE_HTTP 3
Def &INTERNET_FLAG_RELOAD $80000000
Def &INTERNET_FLAG_EXISTING_CONNECT $20000000
Def &INTERNET_FLAG_SECURE $800000
Declare url$, erg&, agent$, internetSession&, internetConnect&, httpOpenRequest&, method$, header$, headers$, data$
Declare bytes#, buffer#, bytes&,path$,server$
Declare ptrAgent&, ptrPath&, ptrServer&, ptrMethod&, ptrHeader&, ptrData&, br$
declare status#, ptrStatusLength&,statusLength&
Dim bytes#, 4
Dim buffer#, 16384
dim status#,32
statusLength& = 32
ptrStatusLength& = addr(statusLength&)
path$ = "v2/oauth/token"
server$ = "login.eveonline.com"
agent$ = "XProfan"
method$ = "POST"
data$ = "{\qkey\q : \qvalue\q}"
br$ = "\r\n"
ptrAgent& = Addr(agent$)
ptrPath& = Addr(path$)
ptrServer& = Addr(server$)
ptrMethod& = Addr(method$)
ptrData& = Addr( data$)
internetSession& = InternetOpen(ptrAgent&, &INTERNET_OPEN_TYPE_PRECONFIG, 0, 0, 0, 0)
internetConnect& = InternetConnect(internetSession&, ptrServer&, &INTERNET_DEFAULT_HTTPS_PORT, 0, 0, &INTERNET_SERVICE_HTTP, 0, 0)
httpOpenRequest& = HttpOpenRequest(internetConnect&, ptrMethod&, ptrPath&, 0, 0, 0, (&INTERNET_FLAG_RELOAD | &INTERNET_FLAG_SECURE ), 0)
header$ = "Authorization: Basic <Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=>"+br$+"Content-Type: application/x-www-form-urlencoded"+br$+"Host: login.eveonline.com" +br$+br$
ptrHeader& = Addr(header$)
HttpSendRequest(httpOpenRequest&, ptrHeader&, len(header$), ptrData&,len(data$))
InternetReadFile(httpOpenRequest&, buffer#, 8192, bytes#)
HttpQueryInfo(httpOpenRequest&, 19, status#, ptrStatusLength&, 0)
print "HTTP Status-Code: "+String$(status#,0)
bytes& = long(bytes#, 0)
Print "\nBytes: " + str$(bytes&)
Print "Antwort:"+String$(buffer#,0)
InternetCloseHandle(httpOpenRequest&)
InternetCloseHandle(internetConnect&)
InternetCloseHandle(internetSession&)
WaitInput
Alles anzeigen
Hallo Jens-Arne!
Über die Suche findest du Folgendes in der Hilfe:
"Diese Systemvariable enthält nach einem Klick auf ein untergeordnetes Fensterelement (Dialogelement) die entsprechende Messagenummer"
Ich habe es mal ganz naiv ausprobiert:
Cls
Declare btn1&
btn1& = Create("Button",%hwnd,"Test",0,0,100,30)
While 1
sleep 10
If %ChildMessage > 0
locate 10,1
Print str$(%ChildMessage) + " "
EndIf
EndWhile
Die Message kann hier zwischen Links, Mittel- und Rechtsklick unterscheiden (513, 519, 516 WM_...BUTTONDOWN), in Verbindung mit einem Waitinput hat es nicht mehr funktioniert. Da weder clicked() noch getfocus() bei Rechts und Mittelklick funktionieren wird es hier aber schwierig herauszufinden, welcher Button geklickt wurde ...
Im Kontext von XProfEd fragt es einen Rechtsklick auf jedes beliebige Element ab, nicht aber %hwnd selbst.
Hm... bei mir wurde die Datei vorher nicht gelöscht und nach dem Fix wurde sie gelöscht. Dann musst du etwas granularer debuggen. Das Löschen des Array Eintrages hatte ich zumindest validieren können, das hatte geklappt.
Mit den Datei-Funktionen habe ich letztens auch gekämpft. Bei mir war es das Problem, dass bei Rewrite durch weniger Daten als zuvor eben nicht (wie gewünscht und erwartet) der bisherige Dateiinhalt verloren ging sondern der Überhang übrig blieb, weshalb ich dann auch so Erase gewechselt bin.
In deinem Fall scheint es Folgendes zu sein:
Erase möchte nicht mit jedem Dateimodus (OpenRW?) zusammenarbeiten. Folgendes hilft:
Schließe die Datei, wenn du mit Lesen oder schreiben fertig bist und lösche sie dann erst mit einem eigenen Assign:
Da ich bei mir irgendwie den Debugger nie zum Laufen bringe, arbeite ich normalerweise mit einer eigenen Konsolenausgabe oder in einfachen Fällen auch mal mit messageboxes als Breakpoints, so konnte man relativ schnell feststellen, dass die Datei zu keinem Zeitpunkt gelöscht wurde.
Wie soll ich es nennen... ein Batchkonverter, ein Photoshop für Filter oder eine Spielwiese für Bildeffekte?
In XProfan habe ich eine kleine Entwicklungsumgebung gebaut, mit der sich OpenGL Shader (GLSL) entwerfen und testen lassen. Die Grundidee war eigentlich, eigene Shader schneller entwerfen und testen zu können.
Wie geht es?
Der Shadercode bezieht sich immer genau auf einen Pixel und die Ausführung passiert parallel. Ich kann natürlich trotzdem umliegende Originalpixel auslesen. Vec2, vec3, vec4 bündeln jeweils mehrere Werte wie Bildkoordinaten, rgb oder rgba Farben, es gibt viele Hilfsfunktionen wie mix, clamp etc. Die Änderungen sind live beim Tippen sichtbar und können als Shader und das Ergebnis als Bilddatei abgespeichert werden.
Es gibt auch die Möglichkeit des Konsolenaufrufs, um direkt Bilder zu konvertieren. Die Syntax bekommt man angezeigt sobald man es in der Eingabeaufforderung mit einem beliebigen Parameter öffnet.
Den aktuellen Download werde ich immer hier speichern, aktuell ist EXE und Quellcode dabei.
https://xprofan.net/intl/de/quellt…980&pg=-1#98980
Viel Spaß beim Ausprobieren und gebt mir gerne Feedback!
Ich habe mal ein wenig herumprobiert. Es sollte mit CreateCoreWebView2Environment losgehen aber das erwartet direkt als Parameter ein COM Objekt. Das wirst du in XProfan leider nicht zum Laufen bringen.
Genau, das war C++. Deine Schreibweise für die Strings funktioniert.Ich habe die Shader-Skripte aber nun in Dateien ausgelagert.
Ich lasse testweise Perlin-Noise generieren. Dafür hatte ich zuvor einen CPU-Code für XProfan, welcher mehrere Minuten pro Bild gebraucht hat, jetzt sind 1000 Bilder pro Sekunde möglich ebenso ziemlich jeder andere Bildeffekt wie Weichzeichnen, Schwarzweiß, Sättigung, Kontrast usw.
Danke für deine Antwort. Andere Pointer waren es nicht aber eine zusätzlicher am vorhandenen.
Auf den ersten Blick sieht es gut aus, ich brauche einen Pointer auf einen Pointer bzw die Speicheradresse des Integers mit der Speicheradresse...
statt:
call(glShaderSource&, vertexShader&, 1, Addr(vertexShaderSource$),0)
scheint es das zu sein:
addr& = Addr(vertexShaderSource$)
addrAddr& = Addr(addr&)
call(glShaderSource&, vertexShader&, 1,addrAddr&,0)
Es ist wohl unüblich einen Pointer auf einen String direkt zu verwenden... wieder etwas gelernt und mir ist nun auch klar, warum in der Definitionsbeschreibung zwei Sternchen vor dem Parameter waren.
Das eof$ hatte ich nur verwendet, da ich es testweise mit $chr(10) und/oder $chr(13) befüllt hatte.
Ich poste hier gerne weitere Fortschritte aber es wird noch ein längerer Weg
Gerne möchte ich in XProfan Shader einsetzen für grafische Effekte aber auch für mathematische Berechnungen, welche dann parallel auf tausenden Kernen der Grafikkarte ausgeführt werden können.
In C++ habe ich es hinbekommen, in XProfan versperrt sich eine Funktion aus mir unbekannten Gründen. Es ist üblich, dass ich OpenGL Funktionen, die nicht zum Basisumfang gehören per wglGetProcAddress verfügbar machen muss und diese dann mit call() ausführe. Ich denke nicht, dass wir hier ein 32/64 Bit Parameterproblem haben, dennoch stürzt der Aufruf immer ab, unter Verdacht immer der übergebene String bzw. die Adresse davon aber dort habe ich schon alles probiert... Ich bekomme glShaderSource nur ausgeführt, wenn ich ihm sage, dass er 0 Quelltexte verarbeiten soll (3. Parameter in Call, 2. der Funktion) oder absichtlich eine nicht existente Shader-ID übergebe (2. Parameter in Call, 1. der Funktion).
Es sollten eigentlich keine OpenGL spezifischen Probleme sein, sobald OpenGL initialisiert ist sollte es klappen - oder selbst bei einem Fehler nicht abstürzen. Der Absturz deutet darauf hin, dass etwas mit den Parametern oder der Stringverarbeitung nicht klappt.
WindowStyle 1+2+8+16
WindowTitle "shader"
Window 100,100 - 720,480 'kompatible Schreibweise
Declare shaderProgram&, vertexShader&, vertexShaderSource$,eof$
oGL("Init", %hWnd, 1,1,1,1)
'Shader Funktionsadressen
Declare glCreateShader&, glShaderSource&
glCreateShader& = oGL("wglGetProcAddress","glCreateShader")
glShaderSource& = oGL("wglGetProcAddress","glShaderSource")
If ( glCreateShader& = 0) | ( glShaderSource& = 0)
Print "Funktion nicht gefunden" : WaitKey : End
Else
Print "Alle Funktionen gefunden"
EndIf
eof$ = chr$(13) + chr$(10) 'ausgelagert, um andere Varianten testen zu können
vertexShaderSource$ = "#version 330 core"+eof$ \
+"layout (location = 0) in vec3 aPos;"+eof$ \
+"layout (location = 1) in vec2 aTexCoord;"+eof$ \
+"out vec2 TexCoords;"+eof$ \
+"out vec3 FragPos;"+eof$ \
+"void main() {"+eof$ \
+"gl_Position = vec4(aPos, 1.0);"+eof$ \
+"TexCoords = aTexCoord;"+eof$ \
+"FragPos = aPos;"+eof$ \
+"}"
vertexShader& = Call(glCreateShader&, $8B31) ' $8B31 = GL_VERTEX_SHADER der erste Shader bekommt normalerweise die ID 1, das passt
Print "Hier ist die Welt noch in Ordnung (Taste drücken für Absturz)"
WaitKey
call(glShaderSource&, vertexShader&, 1, Addr(vertexShaderSource$),0)
'void glShaderSource( GLuint shader,
' GLsizei count,
' const GLchar **string,
' const GLint *length);
'
'shader
'Specifies the handle of the shader object whose source code is to be replaced.
'
'count
'Specifies the number of elements in the string and length arrays.
'-> 1 ist hier ok!
'
'string
'Specifies an array of pointers to strings containing the source code to be loaded into the shader.
'alles probiert, Bereich maßlos überdimensioniert, Addr() vopn Stringvariable, mit zusätzlichen Nullbytes oder normal, Berich per String oder Char gefüllt, verschiedene Zeilenumbrüche
'
'length
'Specifies an array of string lengths.
'0 lässt automatisch ermitteln aber auch eine Angabe macht es nicht besser
Print "Das hier wird nicht erreicht"
WaitKey
oGL("Done")
End
Alles anzeigen
Verusch gerne mal das jpeg mit einem anderen Programm abzuspeichern, eigentlich sollte es gehen.
Hallo,
ich hoffe das Thema ist noch relevant! Genau ein Quad braucht es noch. Optional kann man per oGLW2H das Seitenverhältnis von OpenGL so wählen, dass eine Quadrat bildschirmfüllend wird und man sich nicht mehr um das Seitenverhältnis kümmern muss. Der Kameraabstand muss ausprobiert werden (Code 1) oder könnte per glOrtho durch Parallelperspektive erübrigt werden (Code 2).
Option 1:
Standardperspektive mit ausprobiertem Kameraabstand:
Declare texture%, Handle hg
WindowTitle "Happy Day"
Window 800, 600
hg = Create("hPic", -1, "2061155.jpg")
oGL("Init", %hWnd, 0, 0, 0, 0)
oGL("PosMode", 1)
Set("oGLW2H", 1) 'Verhältnis 1:1 sorgt dafür, dass ein quadratisches Quad den Bildschirm ausfüllt
texture% = oGL("GetTextureBMP", hg, 3)
oGL("Texture", texture%, 1)
DrawScene()
While 1
WaitInput
DrawScene()
EndWhile
PROC DrawScene
oGL("Clear")
oGL("Origin", 0.0, 0.0, -2.41) 'ausprobierter Abstand
oGL("Quad",2,2)
oGl("Show")
ENDPROC
oGL("Done")
End
Alles anzeigen
Option 2:
Orthogonale Ansicht passt immer aber die Hilfsfunktion für die Datentypumwandlung ist etwas sperrig:
Declare texture%, Handle hg
WindowTitle "Happy Day"
Window 800, 600
$H opengl.ph
hg = Create("hPic", -1, "2061155.jpg")
oGL("Init", %hWnd, 1, 0, 0, 0)
oGL("PosMode", 1)
texture% = oGL("GetTextureBMP", hg, 3)
oGL("Texture", texture%, 1)
DrawScene()
While 1
WaitInput
DrawScene()
EndWhile
Proc glOrtho
Parameters a&,b&,c&,d&,e&,f&
'left, right, bottom, top, zNear, zFar
Declare a#
Dim a#,48
Float a#,0 =a&
Float a#,8 =b&
Float a#,16=c&
Float a#,24=d&
Float a#,32=e&
Float a#,40=f&
ogl("glOrtho",\
Long(a#,0 ),long(a#,4 ),\
Long(a#,8 ),long(a#,12),\
Long(a#,16),long(a#,20),\
Long(a#,24),long(a#,28),\
Long(a#,32),long(a#,36),\
Long(a#,40),long(a#,44)\
)
EndProc
Proc DrawScene
oGL("Clear")
oGL("glMatrixMode", ~GL_PROJECTION)
oGL("glLoadIdentity")
glOrtho(-1.0, 1.0, -1.0, 1.0, 0.1, 100.0)
oGL("glMatrixMode",~GL_MODELVIEW)
oGL("Quad",2,2)
oGl("Show")
EndProc
oGL("Done")
End
Alles anzeigen
Du magst Recht haben. Ich hatte erwartet, dass jedes Mal ein neues Treeview erstellt wird und lediglich der Verweis im Handle auf das neue geändert wird aber das Alte scheint tatsächlich auch so zu verschwinden. Mit dem Aufräumen vor Programmende bin ich mir auch immer unsicher, ob es wirklich nötig ist ... das hPic zu löschen sobald es angewendet wurde ist aber gängige Praxis auch in der XProfan Hilfe.
Das sieht doch gut aus. Ja. mehrere Imagelisten in einem Treeview wäre schwierig. Hier habe ich zwei Zeilen ergänzt, um alte hPics und Treeviews zu löschen, bevor neue angelegt werden. Wenn du ganz ordentlich bis, machst du vor Programmende noch ein DeleteObject tb
Proc MakeTV
Parameters String picname
Declare Handle tb, pic
pic = Create("hPic", 0, picname)
tb = Create("ImageList", %Bmpy, %BmpY, pic)
case (tv <> 0) : destroywindow(tv)
tv = Create("TreeView", %HWnd, tb, 400, 10, 300, 500)
Header = TreeView("InsertItem",tv, 0, 0, picname)
DeLeteObject pic
WhileLoop 0, GetCount(tb) - 1
TreeView("InsertItem", tv, Header, &LOOP, Str$(&LOOP))
EndWhile
TreeView("SetChildren", tv, header, 1)
EndProc
Alles anzeigen
Wenn man mal angefangen hat ... mit ein paar Handgriffen ist jetzt auch eine Suche möglich. Diese würde natürlich von weiteren versteckten Keywords profitieren, aktuell ist es nur der Emoji Name.
Hier der Quelltext:
Das HTMLWin ist leider Schrott (zumindest unter Windows 10) und stürzt beim Ändern des HTML ab, ebenso beim häufigen Löschen und Neuerstellen. Ich verwende daher die atl.dll, die nutzt zwar genau den gleichen urlalten Internet Explorer aber es geht. Alles getestet im XProfan X4 Interpreter.
Ich habe mit etwas CSS dafür gesorgt, dass sich jedes Emoji mittig in einem 48x48px Quadrat befindet. Die Schriftgröße 48px war dafür übrigens zu groß.
Die Icons speichere ich in einem Sting, das finde ich handlicher als die LONG-Geschichte und trenne diese ebenso wie deren Bezeichnung per explode in Arrays.
In einer Schleife mache ich folgendes:
1. HTML mit nächstem Emoji drin generieren
2. Browserobjekt mit dem neuen Icon füllen
3. Etwas warten, bis wirklich alles aktualisiert ist und der Emoji angezeigt wird
4. "Screenshot" des Browsers aber Zielkoordinaten im Speicher jedes mal eins aufrücken!
Nach der Schleife dann das Bild speichern und eine Imagelist erstellen. Hier habe ich die Imagelist dann in einem Treeview verwendet. Bei der Toolbar war nach 99 Icons Schluss, das scheint ein Limit von Windows oder XProfan zu sein.
Ja, es dauert lang und ist etwas wackelig (man sollte den Rechner am besten in Ruhe arbeiten lassen) aber es ist ja nur ein Hilfstool für den Entwickler, Ziel ist am Ende die Imagelist als BMP oder PNG zu haben und die funktioniert dann reibungslos. Diese ist übrigens mit meiner erweiterten Emoji Liste 54432x48 Pixel groß.
Der Quelltext ist gekürzt auf 3 icons wegen des 10.000 Zeichen Limits des Forums.
Hier komplett mit allen Emojis und fertiger Imagelist.png:
Declare btn1&, browser&, hpic&, imagelist&
Declare breite&,atl2&,x&
Declare emoji_hex$,emoji_text$, emojies$[], text$[],html$, WebControl&
atl2&=usedll("atl.dll")
Def AtlAxWinInit(0) !"atl.dll","AtlAxWinInit"
AtlAxWinInit()
WebControl&= control("AtlAxWin",html$,$50200000,0,0,100,100,%hwnd,0,0,$020000) '$020000
emoji_hex$ = "🥇 🥈 🥉"
emoji_text$ = "1. Platz Medaille#2. Platz Medaille#3. Platz Medaille"
emojies$[] = Explode(emoji_hex$, " ")
text$[] = Explode(emoji_text$, "#")
breite& = SizeOf(emojies$[] ) * 48
WindowTitle "Emojipedia"
WindowStyle 2+8+16
Window 720,480
UseIcon "Gesicht"
'################################# NUR BEI DER ERSTEN AUSFÜHRUNG ########################
IfNot (FileExists("imagelist.png"))
Mcls breite&, 48
WhileLoop 0, SizeOf(emojies$[]) - 1
html$ = "mshtml:<!DOCTYPE html><html><header><meta charset=\qUTF-8\q></header><body><div style=\qmargin-top:-15px; margin-left: -10px;font-family:Segoe UI Emoji; font-size:36px; width:48px; height:48px; line-height:42px; text-align:center\q>"\
+ emojies$[&loop]+ "</div></body></html>"
WebControl&= control("AtlAxWin",html$,$50200000,0,0,100,100,%hwnd,0,0,$020000)
'Etwas Pausieren durch Last, nur Sleep hat leider auch den Browser am Rendern gehindert
WhileLoop 500
x& = rnd(1000)
EndWhile
Sleep 50
StartPaint WebControl&
CopyBmpToMem 0, 0 - 48,48 > &loop * 48, 0
EndPaInt
destroywindow(WebControl&)
EndWhile
hpic& = Create("hPic", 0, "&MEMBMP")
SavePic "imagelist.png", hpic&
DeLeteObject hpic& ' wird nicht mehr benötigt
Mcls 0,0
EndIf
'################################# NUR BEI DER ERSTEN AUSFÜHRUNG ########################
hpic& = Create("hPic", -1, "imagelist.png")
imagelist& = Create("ImageList",48, 48,hpic&)
Var hTV& = Create("TreeView",%hwnd,imagelist&,0,0,Width(%hwnd),Height(%hwnd))
DeLeteObject hpic& ' wird nicht mehr benötigt
Set("Decimals", 0) 'für XProfan 11
Set("FastMode", 1) 'so geht das Füllen deutlich schneller
WhileLoop 0, SizeOf(emojies$[]) - 1
TreeView("InsertItem",hTV&, 0,&loop,str$(&loop+1)+": "+text$[&loop] + " (Hex: "+emojies$[&loop]+")")
EndWhile
Set("FastMode", 0)
WhileNot iskey(27)
Waitinput
EndWhile
FreeDll atl2&
DeLeteObject imagelist&
End
Alles anzeigen
Das ist die Imagelist als PNG:
Was du vor hast ist insgesamt recht knifflig, vielleicht hilft ein ganz anderer Ansatz. Möchtest du eine Art "Emoji"-Tastatur bauen? Was auch immer, könntest du versuchen die Elemente direkt aus dem HTMLWin klickbar zu machen anstatt sie erst in ein Bild umzuwandeln. Normalerweise haben solche Browserobjekte auch Möglichkeit, Events wie Klicks zurückzugeben, leider ist hierfür in der XProfan Hilfe nichts dokumentiert. Ein Ansatz wären viele HTMLWins mit je einem Emoji drin und du fragst den Klick auf das ganze Element ab. Anhand der Mausposition könntest du auch den Klick eines von mehreren Elementen abfragen. Das Problem mit der unterschiedlichen Zeichenbreite könntest du durch fixe Wrapper, eine Tabelle oder Gridview lösen.