diff --git a/include/ui/gtk.h b/include/ui/gtk.h
index 2de38e5724..846d27f13d 100644
--- a/include/ui/gtk.h
+++ b/include/ui/gtk.h
@@ -84,6 +84,7 @@ typedef struct VirtualConsole {
     GtkWidget *menu_item;
     GtkWidget *tab_item;
     GtkWidget *focus;
+    GdkMonitor *monitor;
     VirtualConsoleType type;
     union {
         VirtualGfxConsole gfx;
diff --git a/qapi/ui.json b/qapi/ui.json
index bb06fb6039..6a91c909a2 100644
--- a/qapi/ui.json
+++ b/qapi/ui.json
@@ -1315,13 +1315,37 @@
 # @show-menubar: Display the main window menubar.  Defaults to "on".
 #     (Since 8.0)
 #
+# @connectors: List of physical monitor/connector names where the
+#     GTK windows containing the respective graphics virtual consoles
+#     (VCs) are to be placed. Index of the connector name in the list
+#     directly indicates the id of the VC. For example, with "-device
+#     gtk,connectors.0=DP-1, connectors.1=HDMI-2", a physical display
+#     connected to DP-1 will be the target monitor for VC0 and the one
+#     on HDMI-2 will be the target for VC1. If there is no valid
+#     connector name for a VC, it won't be displayed anywhere in any
+#     situations and the associatedvirtual display will be shown
+#     as disconnected in the guest. If a valid connector name exists for
+#     a VC but its display cable is not plugged in when guest is launched,
+#     the VC won't be displayed at first but will show up on the display
+#     as soon as the cable is plugged in. If the cable is disconnected
+#     again, the VC will immediately be hidden and guest will see its
+#     virtual display disconnected. When this list param is used, there
+#     shouldn't be any missing elements before the last one in the list.
+#     For example, if connectors.3 is specified, then connector names for
+#     connectors.0 through connectors.2 should also be given. Multiple
+#     VCs can have a same connector name. In this case, all of those
+#     VCs will be displayed on the same physical monitor. But multiple
+#     different connector names can't be assigned to one VC.
+#     (Since 8.1)
+#
 # Since: 2.12
 ##
 { 'struct'  : 'DisplayGTK',
   'data'    : { '*grab-on-hover' : 'bool',
                 '*zoom-to-fit'   : 'bool',
                 '*show-tabs'     : 'bool',
-                '*show-menubar'  : 'bool'  } }
+                '*show-menubar'  : 'bool',
+                '*connectors'    : ['str'] } }
 
 ##
 # @DisplayEGLHeadless:
diff --git a/qemu-options.hx b/qemu-options.hx
index f8f384e551..52a2c33836 100644
--- a/qemu-options.hx
+++ b/qemu-options.hx
@@ -2048,7 +2048,7 @@ DEF("display", HAS_ARG, QEMU_OPTION_display,
 #if defined(CONFIG_GTK)
     "-display gtk[,full-screen=on|off][,gl=on|off][,grab-on-hover=on|off]\n"
     "            [,show-tabs=on|off][,show-cursor=on|off][,window-close=on|off]\n"
-    "            [,show-menubar=on|off]\n"
+    "            [,show-menubar=on|off][,connectors.<index>=<connector name>]\n"
 #endif
 #if defined(CONFIG_VNC)
     "-display vnc=<display>[,<optargs>]\n"
@@ -2146,6 +2146,9 @@ SRST
         ``zoom-to-fit=on|off`` : Expand video output to the window size,
                                  defaults to "off"
 
+        ``connectors=<conn name>`` : VC to connector mappings to display the VC
+                                     window on a specific monitor
+
     ``curses[,charset=<encoding>]``
         Display video output via curses. For graphics device models
         which support a text mode, QEMU can display this output using a
diff --git a/ui/gtk.c b/ui/gtk.c
index f7f8a8e8bd..d30b7758c0 100644
--- a/ui/gtk.c
+++ b/ui/gtk.c
@@ -38,6 +38,7 @@
 #include "qemu/cutils.h"
 #include "qemu/error-report.h"
 #include "qemu/main-loop.h"
+#include "qemu/option.h"
 
 #include "ui/console.h"
 #include "ui/gtk.h"
@@ -741,6 +742,39 @@ static void gd_set_ui_size(VirtualConsole *vc, gint width, gint height)
     dpy_set_ui_info(vc->gfx.dcl.con, &info, true);
 }
 
+static void gd_ui_hide(VirtualConsole *vc)
+{
+    QemuUIInfo info;
+
+    vc->gfx.visible = false;
+    info = *dpy_get_ui_info(vc->gfx.dcl.con);
+    info.width = 0;
+    info.height = 0;
+    dpy_set_ui_info(vc->gfx.dcl.con, &info, false);
+}
+
+static void gd_ui_show(VirtualConsole *vc)
+{
+    QemuUIInfo info;
+    GtkDisplayState *s = vc->s;
+    GdkWindow *window = gtk_widget_get_window(vc->gfx.drawing_area);
+
+    info = *dpy_get_ui_info(vc->gfx.dcl.con);
+    info.width = gdk_window_get_width(window);
+    info.height = gdk_window_get_height(window);
+    dpy_set_ui_info(vc->gfx.dcl.con, &info, false);
+
+    if (gd_is_grab_active(s)) {
+        gd_grab_keyboard(vc, "user-request-main-window");
+        gd_grab_pointer(vc, "user-request-main-window");
+    } else {
+        gd_ungrab_keyboard(s);
+        gd_ungrab_pointer(s);
+    }
+
+    vc->gfx.visible = true;
+}
+
 #if defined(CONFIG_OPENGL)
 
 static gboolean gd_render_event(GtkGLArea *area, GdkGLContext *context,
@@ -1306,12 +1340,10 @@ static void gd_menu_switch_vc(GtkMenuItem *item, void *opaque)
     GtkDisplayState *s = opaque;
     VirtualConsole *vc;
     GtkNotebook *nb = GTK_NOTEBOOK(s->notebook);
-    GdkWindow *window;
     gint page;
 
     vc = gd_vc_find_current(s);
-    vc->gfx.visible = false;
-    gd_set_ui_size(vc, 0, 0);
+    gd_ui_hide(vc);
 
     vc = gd_vc_find_by_menu(s);
     gtk_release_modifiers(s);
@@ -1319,10 +1351,7 @@ static void gd_menu_switch_vc(GtkMenuItem *item, void *opaque)
         page = gtk_notebook_page_num(nb, vc->tab_item);
         gtk_notebook_set_current_page(nb, page);
         gtk_widget_grab_focus(vc->focus);
-        window = gtk_widget_get_window(vc->gfx.drawing_area);
-        gd_set_ui_size(vc, gdk_window_get_width(window),
-                       gdk_window_get_height(window));
-        vc->gfx.visible = true;
+        gd_ui_show(vc);
     }
 }
 
@@ -1352,8 +1381,8 @@ static gboolean gd_tab_window_close(GtkWidget *widget, GdkEvent *event,
     VirtualConsole *vc = opaque;
     GtkDisplayState *s = vc->s;
 
-    vc->gfx.visible = false;
-    gd_set_ui_size(vc, 0, 0);
+    gd_ui_hide(vc);
+
     dpy_gl_scanout_disable(vc->gfx.dcl.con);
     gtk_widget_set_sensitive(vc->menu_item, true);
     gd_widget_reparent(vc->window, s->notebook, vc->tab_item);
@@ -1384,20 +1413,14 @@ static gboolean gd_window_state_event(GtkWidget *widget, GdkEvent *event,
     }
 
     if (event->window_state.new_window_state & GDK_WINDOW_STATE_ICONIFIED) {
-        vc->gfx.visible = false;
-        gd_set_ui_size(vc, 0, 0);
+        gd_ui_hide(vc);
         if (vc->gfx.guest_fb.dmabuf &&
             vc->gfx.guest_fb.dmabuf->draw_submitted) {
             vc->gfx.guest_fb.dmabuf->draw_submitted = false;
             graphic_hw_gl_block(vc->gfx.dcl.con, false);
         }
     } else {
-        GdkWindow *window;
-        window = gtk_widget_get_window(vc->gfx.drawing_area);
-        gd_set_ui_size(vc, gdk_window_get_width(window),
-                       gdk_window_get_height(window));
-
-        vc->gfx.visible = true;
+        gd_ui_show(vc);
     }
 
     return TRUE;
@@ -1457,7 +1480,6 @@ static void gd_tab_window_create(VirtualConsole *vc)
 static void gd_menu_untabify(GtkMenuItem *item, void *opaque)
 {
     GtkDisplayState *s = opaque;
-    GdkWindow *window;
     VirtualConsole *vc = gd_vc_find_current(s);
 
     if (vc->type == GD_VC_GFX &&
@@ -1469,10 +1491,205 @@ static void gd_menu_untabify(GtkMenuItem *item, void *opaque)
         gd_tab_window_create(vc);
     }
 
-    window = gtk_widget_get_window(vc->gfx.drawing_area);
-    gd_set_ui_size(vc, gdk_window_get_width(window),
-                   gdk_window_get_height(window));
-    vc->gfx.visible = true;
+    gd_ui_show(vc);
+}
+
+static void gd_window_show_on_monitor(GdkDisplay *dpy, VirtualConsole *vc,
+                                      gint monitor_num)
+{
+    GtkDisplayState *s = vc->s;
+    GdkMonitor *monitor = gdk_display_get_monitor(dpy, monitor_num);
+    GdkRectangle geometry;
+
+    if (!vc->window) {
+        gd_tab_window_create(vc);
+    }
+
+    gdk_window_show(gtk_widget_get_window(vc->window));
+    gd_update_windowsize(vc);
+    gdk_monitor_get_geometry(monitor, &geometry);
+    /*
+     * Note: some compositors (mainly Wayland ones) may not honor a
+     * request to move to a particular location. The user is expected
+     * to drag the window to the preferred location in this case.
+     */
+    gtk_window_move(GTK_WINDOW(vc->window), geometry.x, geometry.y);
+
+    if (s->opts->has_full_screen && s->opts->full_screen) {
+        gtk_widget_set_size_request(vc->gfx.drawing_area, -1, -1);
+        gtk_window_fullscreen(GTK_WINDOW(vc->window));
+    }
+
+    vc->monitor = monitor;
+    gd_ui_show(vc);
+
+    if (vc->window) {
+        g_signal_connect(vc->window, "window-state-event",
+                         G_CALLBACK(gd_window_state_event), vc);
+    }
+
+    gd_update_cursor(vc);
+}
+
+static int gd_monitor_lookup(GdkDisplay *dpy, char *label)
+{
+    GdkMonitor *monitor;
+    int total_monitors = gdk_display_get_n_monitors(dpy);
+    int i;
+
+    for (i = 0; i < total_monitors; i++) {
+        monitor = gdk_display_get_monitor(dpy, i);
+        if (monitor && !g_strcmp0(gdk_monitor_get_model(monitor), label)) {
+            return i;
+        }
+    }
+    return -1;
+}
+
+static gboolean gd_vc_is_misplaced(GdkDisplay *dpy, GdkMonitor *monitor,
+                                   VirtualConsole *vc)
+{
+    GdkWindow *window = gtk_widget_get_window(vc->gfx.drawing_area);
+    GdkMonitor *mon = gdk_display_get_monitor_at_window(dpy, window);
+    const char *monitor_name = gdk_monitor_get_model(monitor);
+
+    if (!vc->monitor) {
+        if (!g_strcmp0(monitor_name, vc->label)) {
+            return TRUE;
+        }
+    } else {
+        if (mon && mon != vc->monitor) {
+            return TRUE;
+        }
+    }
+    return FALSE;
+}
+
+static void gd_vc_add_remove_monitor(GdkDisplay *dpy, GtkDisplayState *s)
+{
+    VirtualConsole *vc;
+    GdkMonitor *monitor;
+    gint monitor_num;
+    int i;
+
+    /*
+     * We need to call gd_vc_is_misplaced() after a monitor is added to
+     * ensure that the Host compositor has not moved our windows around.
+     */
+    for (i = 0; i < s->nb_vcs; i++) {
+        vc = &s->vc[i];
+        if (vc->label) {
+            monitor_num = gd_monitor_lookup(dpy, vc->label);
+            if (monitor_num >= 0) {
+                monitor = gdk_display_get_monitor(dpy, monitor_num);
+                if (gd_vc_is_misplaced(dpy, monitor, vc)) {
+                    gd_window_show_on_monitor(dpy, vc, monitor_num);
+                }
+            } else if (vc->monitor) {
+                vc->monitor = NULL;
+                if (vc->window) {
+                    g_signal_handlers_disconnect_by_func(vc->window,
+                                                 G_CALLBACK(gd_window_state_event),
+                                                 vc);
+                }
+
+                gd_ui_hide(vc);
+                /* if window exist, hide it */
+                if (vc->window) {
+                    gdk_window_hide(gtk_widget_get_window(vc->window));
+                }
+            }
+        }
+    }
+}
+
+static void gd_monitors_reset_timer(void *opaque)
+{
+    GtkDisplayState *s = opaque;
+    GdkDisplay *dpy = gdk_display_get_default();
+
+    gd_vc_add_remove_monitor(dpy, s);
+}
+
+static void gd_monitors_changed(GdkScreen *scr, void *opaque)
+{
+    GtkDisplayState *s = opaque;
+    QEMUTimer *mon_reset_timer;
+
+    mon_reset_timer = timer_new_ms(QEMU_CLOCK_REALTIME,
+                                   gd_monitors_reset_timer, s);
+    timer_mod(mon_reset_timer,
+              qemu_clock_get_ms(QEMU_CLOCK_REALTIME) + 2000);
+}
+
+static VirtualConsole *gd_next_gfx_vc(GtkDisplayState *s)
+{
+    VirtualConsole *vc;
+    int i;
+
+    for (i = 0; i < s->nb_vcs; i++) {
+        vc = &s->vc[i];
+        if (vc->type == GD_VC_GFX &&
+            qemu_console_is_graphic(vc->gfx.dcl.con) &&
+            !vc->label) {
+            return vc;
+        }
+    }
+    return NULL;
+}
+
+static void gd_vc_free_labels(GtkDisplayState *s)
+{
+    VirtualConsole *vc;
+    int i;
+
+    for (i = 0; i < s->nb_vcs; i++) {
+        vc = &s->vc[i];
+        if (vc->type == GD_VC_GFX &&
+            qemu_console_is_graphic(vc->gfx.dcl.con)) {
+            g_free(vc->label);
+            vc->label = NULL;
+        }
+    }
+}
+
+static void gd_connectors_init(GdkDisplay *dpy, GtkDisplayState *s)
+{
+    VirtualConsole *vc;
+    strList *conn;
+    gint monitor_num;
+    gboolean first_vc = TRUE;
+
+    gtk_notebook_set_show_tabs(GTK_NOTEBOOK(s->notebook), FALSE);
+    gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(s->grab_item),
+                                   FALSE);
+    gd_vc_free_labels(s);
+    for (conn = s->opts->u.gtk.connectors; conn; conn = conn->next) {
+        vc = gd_next_gfx_vc(s);
+        if (!vc) {
+            break;
+        }
+        if (first_vc) {
+            vc->window = s->window;
+            first_vc = FALSE;
+        }
+
+        vc->label = g_strdup(conn->value);
+        monitor_num = gd_monitor_lookup(dpy, vc->label);
+        if (monitor_num >= 0) {
+            gd_window_show_on_monitor(dpy, vc, monitor_num);
+        } else {
+            if (vc->window) {
+                 g_signal_handlers_disconnect_by_func(vc->window,
+                                             G_CALLBACK(gd_window_state_event),
+                                             vc);
+            }
+            gd_ui_hide(vc);
+            if (vc->window) {
+                gdk_window_hide(gtk_widget_get_window(vc->window));
+            }
+        }
+    }
 }
 
 static void gd_menu_show_menubar(GtkMenuItem *item, void *opaque)
@@ -1797,7 +2014,7 @@ static gboolean gd_configure(GtkWidget *widget,
     VirtualConsole *vc = opaque;
 
     if (vc->gfx.visible) {
-        gd_set_ui_size(vc, cfg->width, cfg->height);
+        gd_ui_show(vc);
     }
     return FALSE;
 }
@@ -2134,6 +2351,10 @@ static void gd_connect_signals(GtkDisplayState *s)
                      G_CALLBACK(gd_menu_grab_input), s);
     g_signal_connect(s->notebook, "switch-page",
                      G_CALLBACK(gd_change_page), s);
+    if (s->opts->u.gtk.has_connectors) {
+        g_signal_connect(gdk_screen_get_default(), "monitors-changed",
+                         G_CALLBACK(gd_monitors_changed), s);
+    }
 }
 
 static GtkWidget *gd_create_menu_machine(GtkDisplayState *s)
@@ -2515,6 +2736,10 @@ static void gtk_display_init(DisplayState *ds, DisplayOptions *opts)
         opts->u.gtk.show_tabs) {
         gtk_menu_item_activate(GTK_MENU_ITEM(s->show_tabs_item));
     }
+
+    if (s->opts->u.gtk.has_connectors) {
+        gd_connectors_init(window_display, s);
+    }
 #ifdef CONFIG_GTK_CLIPBOARD
     gd_clipboard_init(s);
 #endif /* CONFIG_GTK_CLIPBOARD */
