Bernhard Krenz

Falsche Bild-Größen in WPF

Als WPF-Entwickler, der hin und wieder mit einem Designer zusammen arbeitet, wird man früher oder später mit dem Problem konfrontiert, dass die zugelieferten Grafiken in der Anwendung größer dargestellt werden, als sie abgespeichert wurden.

Dies wird immer dann auftreten, wenn ein Control die Größe des darin enthaltenen Bildes annehmen soll, zum Beispiel ein Button.

Ursache hierfür ist ziemlich sicher, dass der Designer die Grafiken mit einem Apple-Mac erstellt hat. Dieser arbeitet standardmäßig mit 72dpi für die Grafiken, während Microsoft in Windows 96dpi verwendet. Wird das Bild in WPF dargestellt, wird es um den Faktor 96/72 = 1,33 größer dargestellt.
Eine relativ schlechte Lösung wäre, die Bild-Größen statisch im Code zu hinterlegen. Spätere Größenänderung des Bildes müssten dann jedes Mal zusätzlich auch im Code nachgearbeitet werden.

Da ich in meinem Projekten recht häufig mit solchen Konstellationen arbeite, habe ich ein kleines UserControl entwickelt, dass sich diesem Problem annimmt. Beim Öffnen des Bildes wird die Orginalgröße ausgelesen und dem Bild zugewiesen. Der DPI-Wert wird ignoriert.

Das UserControl arbeitet aktuell nur mit einem BitmapSource als Source des Bildes, zum Beispiel eine Pfad zu einem Bild.

Folgend der Code für das Control:

public class DPIAwareImage : Image
{
	static DPIAwareImage()
	{
		// listen to changes to stretch property, and set default value to fill
		StretchProperty.OverrideMetadata(typeof(DPIAwareImage), new FrameworkPropertyMetadata(Stretch.Fill, new PropertyChangedCallback(StretchPropertyChanged)));
		/// listen to changes to source property
		SourceProperty.OverrideMetadata(typeof(DPIAwareImage), new FrameworkPropertyMetadata(new PropertyChangedCallback(SourcePropertyChanged)));
	}

	// check if a Stretch other then Fill is set.
	private static void StretchPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
	{
		if (args.NewValue == null)
		{
			return;
		}

		if ((Stretch)args.NewValue != Stretch.Fill)
		{
			throw new NotSupportedException("DPIAwareImage only works with Stretch=Fill.");
		}
	}

	// check the new image source and set the origin width and height if available
	private static void SourcePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
	{
		var source = args.NewValue as BitmapSource;

		if (source == null)
		{
			return;
		}

		var image = sender as DPIAwareImage;

		image.Stretch = Stretch.Fill;
		image.Width = source.PixelWidth;
		image.Height = source.PixelHeight;
	}
}

In den Zeilen 38 und 39 wird die Orginalgröße der Datenquelle ausgelesen und dem Bild zugewiesen.

Das Control überwacht neben Änderungen der Source-Eigenschaft auch die Stretch-Eigenschaft. Alles außer „Fill“ würde für den Verwendungszweck nicht funktionieren, da sich der möglicherweise „falsche“ DPI-Inhalt an die festgelegte Größe skalieren muss.


Länge/Dauer eines Videos mit MediaInfo ermitteln

Wer versucht die Länge eines Videos C# zu ermitteln, wird im Internet viele Lösungsvorschläge finden. Oftmals sind diese aber unvollständig, funktionieren nicht oder einfach nur unverständlich.

Ich zeige folgend die Nutzung der MediaInfo-Bibliothek.

Vorbereitung:

  1. Die aktuelle Version der MediaInfo-Bibliothek herunterladen
  2. Die MediaInfo.dll selbst, als auch den C# Wrapper in das eigene Projekt integrieren
  3. Die Video-Datei mit Hilfe des Wrappers öffnen, die Länge abfragen und wieder schließen

Download der MediaInfo-Bibliothek

  1. Auf der SourceForge-Seite den Reiter „Files“ auswählen
  2. In der Verzeichnisliste „binary“ auswählen
  3. „libmediainfo0“ auswählen
  4. Den obersten Eintrag/die aktuellste Version auswählen (z.B. „0.7.62“)
  5. Die passende Windows-Version „WithoutInstaller“ downloaden. Für ein 64bit System muss die x64 Version heruntergeladen werden, für ein 32bit die i386
    (z.B. „MediaInfo_DLL_0.7.62_Windows_x64_WithoutInstaller.7z“

Projektintegration

  1. Die heruntegeladene 7z-Datei mit 7-Zip entpacken
  2. Die MediaInfo.dll aus dem Hauptverzeichnis in das Visual-Studio-Projekt auf der Hauptebene integrieren
  3. Im Eigenschaftsfenster der MediaInfo.dll den „BuildVorgang“ auf „Inhalt“ und „In Ausgabeverzeichnis kopieren“ auf „Kopieren, wenn neuer“ stellen
    (Die MediaInfo.dll muss später parallel zur exe der Anwendung liegen.)
  4. In der Zip unter „Developers“->“Source“->“MediaInfoDll“ die „MediaInfoDLL.cs“ (C# Wrapper) an eine beliebige Stelle in das Visual-Studio-Projekt integrieren

Länge der Video-Datei abfragen

var mediaInfo = new MediaInfo();
mediaInfo.Open(fullPath);
var durationInMilliSecondsString = mediaInfo.Get(StreamKind.Video, 0, "Duration");
mediaInfo.Close();

if (durationInMilliSecondsString != null)
{
	var durationInMilliseconds = long.Parse(durationInMilliSecondsString);
}

Offene Verbindungen zu einer SQL-Datenbank trennen

Ab und zu steht man vor der Herausforderung eine SQL-Datenbank wiederherzustellen oder sonstwie zurück zu setzen. Dann wird man schnell mit dem Problem konfrontiert, dass offene Verbindung zur Datenbank diese Aufgabe verhindern.

Nun kann man entweder mit den üblichen Tools, wie dem SQL-Management-Studio, die Verbindungen trennen, oder man bastelt sich dazu ein kleines Skript. Da ich selber hauptsächlich skriptbasiert arbeite, habe ich dafür folgenden Algorithmus entwickelt:

USE master

DECLARE @databaseName VARCHAR(15)
SET @databaseName = 'name_of_your_database'

DECLARE @kill_id INT
DECLARE @disconnectQuery NVARCHAR(255)
DECLARE killprocess_cursor CURSOR FOR SELECT a.spid FROM sysprocesses a JOIN sysdatabases b ON a.dbid = b.dbid WHERE b.name = @databaseName
OPEN killprocess_cursor

FETCH NEXT FROM killprocess_cursor INTO @kill_id
WHILE @@FETCH_STATUS = 0
BEGIN
	SET @disconnectQuery = 'KILL '+ CONVERT(VARCHAR, @kill_id)
	RAISERROR ('EXECUTING: %s', 1, 1, @disconnectQuery) WITH NOWAIT
	EXEC (@disconnectQuery)
	FETCH NEXT FROM killprocess_cursor INTO @kill_id
END	

CLOSE killprocess_cursor
DEALLOCATE killprocess_cursor

Das Skript durchläuft mit einem Cursor sämtliche offenen Prozesse der festgelegten Datenbank und schließt diese mit Hilfe des „Kill“-Befehls und der zugehörigen Prozess-Id.


if (![nil isEqualToString:nil]) { }

Das folgende Verhalten entspricht der Apple’s Objective-C 2.0 Dokumentation, ist aber möglicherweise nicht jedermann bekannt.

Bei einem Vergleich zweiter Strings in iOS mit isEqualToString, wird das „do something“ im folgenden Codeschnipsel auch ausgeführt, wenn beide Argumente nil sind, obwohl diese auf den ersten Blick gleich erscheinen.

if (![string1 isEqualToString:string2])
{
 // do something
}

Das liegt daran, dass eine Methode zu nil immer 0 zurückgibt und als false interpretiert wird. Die if-Bedingung ist damit erfüllt.

Die korrekt Art und Weise das obere Beispiel umzusetzen, wäre also:

if (![string1 isEqualToString:string2] && (string1 != nil || string2 != nil))
{
  // do something
}

Dateiupload mit Windows-Phone 8

Wer versuch mit der API von Windows-Phone (7, 7.5, 8) eine Datei auf einem Webserver hochzuladen, wird schnell merken, dass die einfach zu nutzende WebClient-Klasse hierfür keine Funktionen zur Verfügung stellt.

Wer keine Lust hat in den Tiefen des HttpWebRequest zu irren, der nutzt dazu am Besten vorhandene Bibliotheken der Community. Ein Dateiupload lässt sich zum Beispiel mit RestSharp realisieren.

In meinem Beispiel will ich eine Datei aus einem bestimmten Verzeichnis des IsolatedStorage hochladen.

Schritt 1: Ermittlung des Dateipfades

var directoryName = "FilesToUpload";

string[] files = null;

// get all files from the isolatedstorage in the specified directory
using (var storage = IsolatedStorageFile.GetUserStoreForApplication())
{
	// if the directory does not exists, there is nothing to do
	if (!storage.DirectoryExists(directoryName))
	{
		return;
	}

	files = storage.GetFileNames(Path.Combine(directoryName, "*.xml"));
}

// if there is no file present in the directory,
// there is nothing to do either
if (files == null || files.Length == 0)
{
	return;
}

// use the first file for upload
var filePath = Path.Combine(directoryName, files[0]);

Schritt 2: Formulierung des Webrequests

(Weiterlesen…)


Falsche Größe eines ImageView in einem ListView

Wer in einem Android-ListView Reihen mit unterschiedlicher Höhe hat, zum Beispiel Bilder und Text-Elemente, wird eventuell wie ich auf das Problem stoßen, dass die Reihe für das Bild mit einer falschen Höhe angezeigt wird.

In meinem Fall war diese zu hoch, was je nach Skalierungsverhalten des Bildes entweder zu einer Verzerrung des Bildes oder zu einem Abstand zur nächsten Reihe führte.

Im Adapter des ListViews muss deshalb zur Laufzeit die Höhe des Bildes berechnet und manuell gesetzt werden. Der View dazu wird aus einer Layout-XML in res/layout erzeugt.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent"
	android:orientation="vertical">

	<ImageView
		android:id="@+id/imageView"
		android:layout_width="match_parent"
		android:layout_height="wrap_content"
		android:scaleType="fitXY"
		/>

</LinearLayout>

Die getView-Methode im Adapter könnte wie folgt aussehen. Dies ist natürlich nur der Auszug für den ImageView. Für die anderen Reihen gibt es je nach Position anderen Inhalt.

@Override
public View getView(int position, View convertView, ViewGroup parent)
{
	View view = (LinearLayout) mInflater.inflate(R.layout.image_item, null);
	
	ImageView imageView = (ImageView) view.findViewById(R.id.imageView);

	// load the bitmap for the image source
	String filePath = "xyz"; // you get this from somewhere, depending on the position	
	Bitmap bitmap = Util.loadBitmap((Activity) context, RessourceLoader.getFilesDirectory() + filePath);
	
	if (bitmap != null)
	{
		imageView.setImageBitmap(bitmap);

		// get the dimensions of the current display
		DisplayMetrics dm = new DisplayMetrics();
		((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(dm);
		if (dm != null)
		{
			// calculate height and set it via layout params
			float height = ((float) dm.widthPixels / bitmap.getWidth()) * bitmap.getHeight();
			imageView.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, (int)height));
		}
	}
	
	return view;
}

SQL-Skriptdateien aus Masterskript aufrufen

Bei der Architektur einer Datenbank gilt es nach einer Testphase oder nach Änderungen oftmals diese komplett neu aus den beteiligten Skripten zu erzeugen. Um zu viel manuelle Arbeit zu vermeiden erzeuge ich mir dazu ein weiteres Skript, welches neben der Installation der Datenbank auch alle beteiligten Skripte in fest definierter Reihenfolge ausführt.

Das Ausführen der Skripte aus aus einer Datei erfolgt über den Befehl xp_cmdshell. Dieser macht nichts weiter, als einen Befehl an die Windows-Kommandozeile weiter zu erreichen. Der Befehl ist im Standardfall allerdings sicherheitsbeschränkt, weshalb die Konfiguration vorher geändert werden muss.

Folgende Schritte sind dazu notwendig:

  1. Löschen der Datenbank falls diese existiert
  2. Datenbank neu erstellen
  3. Berechtigung konfigurieren, um Skripte aus Dateien auszuführen
  4. Konstanten deklarieren und initialisieren
  5. Skript-Dateien in eine temporäre Tabelle einfügen
  6. Diese Tabelle in einer Schleife durchlaufen und die Skripte aus den Dateien ausführen

Sehen wir uns einige der Schritte im Detail an:

Schritt 3:

-- To allow advanced options to be changed.
EXEC sp_configure 'show advanced options', 1

GO

-- To update the currently configured value for advanced options.
RECONFIGURE

GO

-- To enable the feature.
EXEC sp_configure 'xp_cmdshell', 1

GO

-- To update the currently configured value for this feature.
RECONFIGURE

Mit diesem Schnipsel werden die erweiterten Berechtigungen für den xp_cmdshell-Befehl gesetzt.

(Weiterlesen…)


Instanz aus Klassennamen erzeugen

Nehmen wir folgenden Fall an: Der Name einer Klasse liegt als string vor und es gilt eine Instanz der Klasse zu erzeugen.

Das kann zum Beispiel so funktionieren:

// you got the name of the class from somewhere
string className = "Message";
// create the type of the class using the destinated namespace
var type = Type.GetType("ValueObjects." + className, false); 
// create the instance now
var message = Activator.CreateInstance(type) as Message;

In ASP.Net funktioniert das allerdings nicht. Hier ist ein Workarund über den BuildManager (using System.Web.Compilation) notwendig:

// you got the name of the class from somewhere
string className = "Message";
// create the type of the class using the destinated namespace
var type = BuildManager.GetType("ValueObjects." + className, false);
// create the instance now
var message = Activator.CreateInstance(type) as Message;

Existiert eine Konstante?

Wer regelmäßig mit iOS arbeitet wird schnell einmal mit dem Problem konfrontiert, dass eine zu verwendende System-Konstante nur in einer neueren iOS-Version verfügbar ist.

Hier stellt sich die Frage, wie zu prüfen ist, ob die Konstante in der iOS-Version des Nutzers überhaupt zur Verfügung steht, um durch eine geeignete Weiche Abstürze zu vermeiden.

Das Ganze zeige ich am Beispiel einer Observer-Konstante des MPMoviePlayerControllers zum Abspielen von Videos, welche einem die Möglichkeit bietet, ein Video erst dann einzublenden, wenn das erste Frame zur Verfügung steht. Denn nur so kann man ein Flickern am Anfang des Videos vermeiden:

if (&MPMoviePlayerReadyForDisplayDidChangeNotification != NULL) // check if constant exists
{
	// >= iOS 6.0, use readyForDisplay property
	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(videoIsVisibleChanged:) name:MPMoviePlayerReadyForDisplayDidChangeNotification object:nil];
}
else
{
	// < iOS 6.0, use playbackState property
	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(videoStateChanged:) name:MPMoviePlayerPlaybackStateDidChangeNotification object:nil];
}

SortedDictionary und ArgumentException

Neulich hatte ich einen sehr merkwürdigen Fehler bei der Verwendung eines SortedDictionary<string, string>. Ich erhielt eine ArgumentException mit der Beschreibung „Ein Eintrag mit dem gleichen Schlüssel ist bereits vorhanden.“

Das Ganze war umso verwunderlicher, da die Daten 1:1 aus einem normalen Dictionary<string, string> übernommen wurden und demzufolge der Fehler schon an dieser Stelle hätte auftreten müssen.

Um auf die Ursache des Fehlers zu kommen, habe ich folgende Testklasse geschrieben:

public class Sorter : IComparer
{
	public Sorter()
	{
		var sortedDictionary = new SortedDictionary<string, string>(this);
		sortedDictionary.Add("test", "Hallo Welt");
		sortedDictionary.Add("Test", "Hallo zurück");
	}

	public int Compare(string x, string y)
	{
		x = x.ToLower();
		y = y.ToLower();

		return x.CompareTo(y);
	}
}

Mach beachte die Zeile 12 und 13. Die Sortierung sollte case-insensitive erfolgen. Dies hat aber zur Folge, dass nicht der Angegebene Schlüssel „Test“ eingefügt, sondern der aus dem Vergleich, nämlich „test“. Ohne diese Zeilen würden der Fehler nicht auftreten.

Für mich ist dieses Verhalten nicht ganz nachvollziehbar und es fühlt sich ganz leicht nach einem Bug an. Denn obwohl die Vergleichsmethode den Wert „0“ für „gleich“ zurück gibt, sollte dennoch der ursprünglich angegebene Schlüssel verwendet werden.


Copyright © 2012-2019 Bernhard Krenz Alle Rechte vorbehalten.