Hola a todos.
Tengo un método en C# que actualiza el contenido de un PictureBox que recibe como parámetro, algo así:
void fun(PictureBox pb){
pb.Image = (Image)this.bitMap; //bitMap es un objeto Bitmap.
}
El caso es que esa función la uso de callback en un Timer, para ir cambiando la imagen cada cierto tiempo.
Si todo el código anterior lo meto en el mismo Windows.Form donde está el PictureBox todo va bien.
El problema es que quiero sacar todo ese código a otra clase, y usar esta clase desde mi Windows.Form. Es cuando hago esto cuando todo falla.
El error que me da es: Unexpected "Bitmap Region is already Locked".
He buscado bastante por internet y he intentado varias cosas:
1 - He probado con Timers.Timer y con Windows.Forms.Timer .
2 - He usado el keyword 'lock' en el método de callback.
3 - He leído algo de Invoke e InvokeRequired. PictureBox tiene el InvokeRequired a 'false' así que imagino que no tiene nada que ver con usar invoke.
¿Alguien sabe qué puedo hacer?
Gracias, saludos.
El mensaje de error se explica por si mismo, te está indicando que se está intentando bloquear la misma imagen simultaneamente (
Bitmap.LockBits) sin haberla desbloqueado previamente (
Bitmap.UnlockBits), y claro está, no puedes bloquear la imagen dos veces consecutivas si no la desbloqueas, comprueba bien lo que estés haciendo con ese objeto '
bitMap'.
EDITO: Tras leer el título de tu pregunta (que antes no me fijé muy bien) afirmas que la aplicación es multi-hilo, entonces probablemente la causa se deba a que varios hilos están intentando acceder/usar la misma imagen simultaneamente, el problema sigue siendo el mismo que he mencionado arriba.
Prueba a utilizar el método
Bitmap.Clone para evitar esa excepción.
⇲- Bitmap.LockBits Method (http://msdn.microsoft.com/en-us/library/System.Drawing.Bitmap.LockBits%28v=vs.110%29.aspx)
- Bitmap.UnlockBits Method (http://msdn.microsoft.com/en-us/library/system.drawing.bitmap.unlockbits%28v=vs.110%29.aspx)
- Bitmap.Clone Method (http://msdn.microsoft.com/en-us/library/system.drawing.bitmap.clone%28v=vs.110%29.aspx)
Cita de: SARGE553413 en 6 Octubre 2014, 00:00 AM3 - He leído algo de Invoke e InvokeRequired. PictureBox tiene el InvokeRequired a 'false' así que imagino que no tiene nada que ver con usar invoke.
Si tu aplicación es single-threaded no tienes porque preocuparte desde que Class (Form) asignes la propiedad de cualquier control,
de lo contrario, si tu aplicación es multi-htreaded y estás intentando modificar la propiedad de ese control desde un thread distinto al que creó el control, entonces por supuestisimo debes usar
Control.InvokeRequired +
Control.Invoke.
Aparte, en lo referente al
Timer, no veo razón para que te estés complicando la vida con un
Threading.Timer (+ Callbacks), el cual si tu aplicación es multi-hilo entonces es probable que también cause un error derivado con la imagen, ¿es realmente necesario para tu propósito?, prueba a utilizar un
Windows.Forms.timer en su lugar.
saludos
Hola de nuevo, gracias por la respuesta. Primero debo aclarar que estoy seguro de que desbloqueo el bitmap.
He probado una cosa que funciona: desde el fichero fuente del formulario que uso (Form1) hago un método que usa un Timers.Timer y muestra bien las imágenes (boca abajo, no se por qué).
También he probado un método que en lugar de un Timer es un bucle con un Thread::Sleep().
Repito, de estas dos formas funciona bien.
Si traslado cualquiera de estas 2 formas a otra clase todo falla, no entiendo por qué.
Voy a copiar el código en C++/CLI (es exactamente igual que C#, sintaxis aparte), de los métodos que uso, sin Timer:
Form1 (sin usar clase aparte)
//Esta cabecera es para poder usar el método como callback del timer.
void Form1::showMyImage2(Object ^source, ElapsedEventArgs ^e){
try{
int len = 0;
unsigned char *c = this->getFrame2((String^)Form1::path + Convert::ToString(this->counter) + ".bmp", len);
BitmapData ^bmpd = this->bmp->LockBits(Drawing::Rectangle(0, 0,
this->width, this->height), ImageLockMode::ReadWrite,this->pixelFormat);
bmpd->Scan0 = (IntPtr)c;
this->bmp->UnlockBits(bmpd);
this->picBox->Image = dynamic_cast<Image^>(this->bmp);
//Esto es para ir cambiando del frame 0 al 1, al 2 ... etc.
this->counter = (this->counter == 5 ? 0 : this->counter + 1);
}
catch (Exception ^e){
Console::WriteLine(e->InnerException);
Console::WriteLine(e->Message);
Console::WriteLine(e->StackTrace);
}
}
array<unsigned char>^ Form1::getFrame2(String ^path, int %len){
FileStream ^fs = gcnew FileStream(path, FileMode::Open, FileAccess::Read);
BinaryReader ^br = gcnew BinaryReader(fs);
FileInfo ^aux = gcnew FileInfo(path);
int nBytes = aux->Length;
Console::WriteLine("long: {0}, int: {1}", nBytes, (int)nBytes);
array<unsigned char> ^buffer = br->ReadBytes(nBytes);
len = nBytes;
pin_ptr<unsigned char> p = &buffer[0];
unsigned char *p2 = p;
return p2;
}
La otras otras clases, AbstractLiveView y LiveView. AbstracLiveView tiene un método abstracto "getNextFrame()" que implementa LiveView. Estas clases preteden sustituir el código de Form1.
AbstractLiveView
//Esta cabcera es por usar este método como callback de un timer.
void AbstractLiveView::updateFrame(Object ^source, EventArgs ^e){
try{
this->bmpData = this->bmp->LockBits(Drawing::Rectangle(0, 0, this->width, this->heigth),
ImageLockMode::ReadWrite, this->pixelFormat);
//Método abstracto implementado en LiveView (abajo)
this->bmpData->Scan0 = this->getNextFrame();
this->bmp->UnlockBits(this->bmpData);
this->picBox->Image = dynamic_cast<Image^>(this->bmp);
this->picBox->Update();
}
catch (Exception ^e){
Console::WriteLine(e->Message);
Console::WriteLine(e->StackTrace);
}
}
void AbstractLiveView::startLiveView(int frequency){
for (int i = 0; i <= 5; i++){
this->updateFrame(nullptr,nullptr);
}
}
LiveView (Implementación de getNextFrame())
//Obtiene el siguiente frame a dibujar (lo coge del disco duro)
IntPtr LiveView::getNextFrame(){
pin_ptr<unsigned char> p = nullptr;
try{
String ^path = pathImage + Convert::ToString(this->counter) + ".bmp";
FileStream ^fs = gcnew FileStream(path, FileMode::Open, FileAccess::Read);
BinaryReader ^br = gcnew BinaryReader(fs);
FileInfo ^aux = gcnew FileInfo(path);
uint nBytes = aux->Length;
array<unsigned char> ^buffer = br->ReadBytes(nBytes);
p = &buffer[0];
unsigned char *p2 = p;
this->counter = (this->counter == 5 ? 0 : this->counter + 1);
return (IntPtr)p;
}
catch (Exception ^e){
Console::WriteLine(e->Message);
Console::WriteLine(e->StackTrace);
return (IntPtr)p;
}
}
Saludos y gracias.
Como he comentado en otras ocasiones C++/Cli no es mi fuerte, quizás me equivoque en algún aspecto de lo que voy a comentar a continuación, pero he notado cierta irregularidad aquí:
FileStream ^fs = gcnew FileStream(path, FileMode::Open, FileAccess::Read);
BinaryReader ^br = gcnew BinaryReader(fs);
Me he fijado en el operador gcnew, y me he informado un poco sobre su funcionalidad, pero al parecer dicho operador lo único que hace es indicar que los objetos se deben auto liberar, eso no significa que se vayan a liberar inmediatamente despues de enviar el return value, el garbage collector libera recursos cuando así lo cree necesario (a menos que lo invoques directamente), puede ser inmediatamente, o pueden pasar 2 o 20 segundos, aunque, con el uso de ese operador no estoy nada seguro la verdad, pero prueba a añadirle un bloque Finally a tu Try/catch para liberar los objetos:
Dim fs As FileStream = Nothing
Dim br As BinaryReader = Nothing
Try
fs = New FileStream("", FileMode.Open, FileAccess.Read)
br = New BinaryReader(fs)
Catch ex As Exception
Finally
If fs IsNot Nothing Then
fs.Close()
End If
If br IsNot Nothing Then
br.Close()
End If
End Try
C# (conversión al vuelo):
FileStream fs = null;
BinaryReader br = null;
try {
fs = new FileStream("", FileMode.Open, FileAccess.Read);
br = new BinaryReader(fs);
} catch (Exception ex) {
} finally {
if (fs != null) {
fs.Close();
}
if (br != null) {
br.Close();
}
}
Lo mismo digo en el bloque Try/Catch donde usas el método LockBits, ¿que ocurre si hubiera un error de por medio y no se procesa la instrucción UnLockBits?,
yo directamente eliminaría el Try/catch, y en caso de encontrar una excepción la intentaría resolver, pero bueno, si quieres usar Try/catch entonces es tu deber asegurarte de que la imagen se desbloquea siempre en cualquier circunstancia:
Dim bmpData As BitmapData = Nothing
Dim bmp As Bitmap = Nothing
Dim success As Boolean = False
Try
bmp = Bitmap.FromFile("")
bmpData = bmp.LockBits(New Rectangle With {.Width = bmp.Width, .Height = bmp.Height},
ImageLockMode.ReadWrite, bmp.PixelFormat)
bmp.UnlockBits(bmpData)
success = True
Catch ex As Exception
Finally
If Not success Then
bmp.UnlockBits(bmpData)
End If
End Try
C# (conversión al vuelo):
BitmapData bmpData = null;
Bitmap bmp = null;
bool success = false;
try {
bmp = Bitmap.FromFile("");
bmpData = bmp.LockBits(new Rectangle {
Width = bmp.Width,
Height = bmp.Height
}, ImageLockMode.ReadWrite, bmp.PixelFormat);
bmp.UnlockBits(bmpData);
success = true;
} catch (Exception ex) {
} finally {
if (!success) {
bmp.UnlockBits(bmpData);
}
}
No se si esos pequeños ajustes resolverán el problema, pero tampoco puedo ayudar mucho más.
Saludos!
Se me ha ocurrido una posible solución, de forma más simple.
SyncLock Statement - MSDN (http://msdn.microsoft.com/en-us/library/3a86s51t.aspx)
CitarThe SyncLock statement ensures that multiple threads do not execute the statement block at the same time. SyncLock prevents each thread from entering the block until no other thread is executing it.
Utilízalo en el bloque donde haces las operaciones con el bitMap, ejemplo:
Sub GetFrame
SyncLock bitMap
' hacer aquí operaciones con el bitMap... lockbits, unlockbits, etc
End SyncLock
End sub
o sino, prueba con el bitmapData.
Saludos!